diff --git a/.gitignore b/.gitignore index 9a33506..867c67a 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,7 @@ npm-debug.log yarn-error.log ###< symfony/webpack-encore-bundle ### + +###> liip/imagine-bundle ### +/public/media/cache/ +###< liip/imagine-bundle ### diff --git a/assets/controllers/tabs_controller.js b/assets/controllers/tabs_controller.js index 15f45ce..d418abb 100644 --- a/assets/controllers/tabs_controller.js +++ b/assets/controllers/tabs_controller.js @@ -24,8 +24,13 @@ export default class extends Controller { }) this.buttonTargets.forEach((element, index) => { - if (this.hasButtonActiveClass) { - element.classList.toggle(this.buttonActiveClass, activeTab === index) + if (false === this.hasButtonActiveClass) { + return + } + if (activeTab === index) { + element.classList.add(...this.buttonActiveClasses) + } else { + element.classList.remove(...this.buttonActiveClasses) } }) } diff --git a/assets/controllers/upload_collection_controller.js b/assets/controllers/upload_collection_controller.js new file mode 100644 index 0000000..89cc23e --- /dev/null +++ b/assets/controllers/upload_collection_controller.js @@ -0,0 +1,63 @@ +import { Controller } from '@hotwired/stimulus' +import { useFetch } from '../mixins/use_fetch' +import Dropzone from 'dropzone' +import 'dropzone/dist/dropzone.css' +Dropzone.autoDiscover = false + +/* stimulusFetch: 'lazy' */ +export default class extends Controller { + + static targets = [ + 'dropzone', + ] + + static values = { + endpointUpload: String, + endpointDelete: String, + maxFiles: Number, + maxFilesize: Number, + acceptedFiles: String, + chunking: Boolean, + params: Object, + } + + connect () { + useFetch(this) + + const dropzone = new Dropzone(this.dropzoneTarget, { + url: this.endpointUploadValue, + withCredentials: true, + maxFiles: this.maxFilesValue, + maxFilesize: this.maxFilesizeValue, + acceptedFiles: this.acceptedFilesValue, + chunking: this.chunkingValue, + chunkSize: 10000000, + parallelUploads: 3, + retryChunks: true, + disablePreviews: false, + createImageThumbnails: true, + autoProcessQueue: true, + addRemoveLinks: true, + }) + + dropzone.on('sending', (file, xhr, formData) => { + if (this.hasParamsValue) { + let keys = Object.keys(this.paramsValue) + keys.forEach(key => { + formData.append(key, this.paramsValue[key]) + }) + } + formData.append('uuid', file.upload.uuid) + formData.append('originalFilename', file.name) + window.dispatchEvent(new CustomEvent('dropzone:uploading')) + }) + + dropzone.on('queuecomplete', () => { + window.dispatchEvent(new CustomEvent('dropzone:complete')) + }) + + dropzone.on('removedfile', (file) => { + fetch(`${this.endpointDeleteValue}?uuid=${file.upload.uuid}`, { method: 'delete', credentials: 'include' }) + }) + } +} diff --git a/assets/controllers/upload_controller.js b/assets/controllers/upload_controller.js deleted file mode 100644 index c50e865..0000000 --- a/assets/controllers/upload_controller.js +++ /dev/null @@ -1,152 +0,0 @@ -import { Controller } from '@hotwired/stimulus' -import Dropzone from 'dropzone' -Dropzone.autoDiscover = false - -/* stimulusFetch: 'lazy' */ -export default class extends Controller { - - static targets = [ - 'dropzone', - 'queue', - 'previewTemplate', - 'fieldsTemplate', - 'fields', - 'error', - 'errorMessage', - 'upload', - ] - - static values = { - endpoint: String, - maxFiles: Number, - maxFilesize: Number, - acceptedFiles: String, - chunking: Boolean, - language: Object, - params: Object, - uploads: Array, - error: String, - formName: String, - } - - initialize() { - this.fieldsTemplate = this.fieldsTemplateTarget.innerHTML - } - - connect () { - const dropzone = new Dropzone(this.dropzoneTarget, { - url: this.endpointValue, - withCredentials: true, - maxFiles: this.maxFilesValue, - maxFilesize: this.maxFilesizeValue, - acceptedFiles: this.acceptedFilesValue, - chunking: this.chunkingValue, - chunkSize: 10000000, - parallelUploads: 3, - retryChunks: true, - previewsContainer: this.queueTarget, - previewTemplate: this.previewTemplateTarget.innerHTML, - createImageThumbnails: true, - dictFileTooBig: this.languageValue.fileTooBig, - dictInvalidFileType: this.languageValue.invalidFileType, - dictMaxFilesExceeded: this.languageValue.maxFilesExceeded, - }) - - dropzone.on('addedfile', file => { - if (this.maxFilesValue > this.uploadsValue.length) { - this._removeFile(file.id) - return - } - this.errorValue = '' - const uploads = this.uploadsValue; - uploads.pop() - this.uploadsValue = uploads - }) - - dropzone.on('removedfile', file => { - this._removeFile(file.upload.uuid) - }) - - dropzone.on('maxfilesexceeded', file => { - this.errorValue = dropzone.options.dictMaxFilesExceeded - dropzone.removeFile(file); - }) - - dropzone.on('error', (file, errorMessage) => { - this.errorValue = errorMessage; - dropzone.removeFile(file) - }) - - dropzone.on('dragenter', () => { - this.errorValue = '' - }) - - dropzone.on('success', file => { - const response = JSON.parse(file.xhr.response) - const uploads = this.uploadsValue - uploads.push({ - filename: response.filename, - originalFilename: response.originalFilename, - uploaderType: response.uploaderType, - size: response.size, - mimeType: response.mimeType, - fileId: file.upload.uuid - }) - this.uploadsValue = uploads - window.dispatchEvent(new CustomEvent('dropzone', { - detail: 'success' - })) - }) - - dropzone.on('sending', () => { - window.dispatchEvent(new CustomEvent('dropzone', { - detail: 'uploading' - })) - }) - } - - errorValueChanged () { - if ('' === this.errorValue) { - this.errorTarget.classList.add('hidden') - } else { - this.errorMessageTarget.innerText = this.errorValue - this.errorTarget.classList.remove('hidden') - } - } - - uploadsValueChanged (uploads) { - this.fieldsTarget.innerHTML = '' - uploads.forEach((upload, index) => { - const field = this.fieldsTemplate - .replace(/__formName__/g, this.formNameValue) - .replace(/__index__/g, index) - .replace(/__fileId__/g, upload.fileId) - .replace(/__originalFilename__/g, upload.originalFilename) - .replace(/__filename__/g, upload.filename) - .replace(/__mimeType__/g, upload.mimeType) - .replace(/__size__/g, upload.size) - this.fieldsTarget.insertAdjacentHTML('beforeend', field) - }) - } - - removeFile ({ params }) { - const fileId = params.fileid - this._removeFile(fileId) - } - - _removeFile(fileId) { - this.uploadsValue = this.uploadsValue.filter(upload => { - return upload.fileId !== fileId - }) - } - - removeUpload (event) { - event.preventDefault() - this.uploadTargets.forEach(element => { - if (element.contains(event.target)) { - element.remove() - } - }) - } - -} diff --git a/assets/styles/components/button.css b/assets/styles/components/button.css index d22f4f2..10d045a 100644 --- a/assets/styles/components/button.css +++ b/assets/styles/components/button.css @@ -1,7 +1,11 @@ .btn { - @apply inline-flex justify-center rounded-lg text-sm uppercase font-semibold py-2.5 px-4 bg-primary text-white hover:bg-primary/80 w-full; + @apply inline-flex justify-center rounded-lg text-sm uppercase font-semibold py-2.5 px-4 bg-primary text-white hover:bg-primary/80 w-full shadow-md; } .btn--secondary { @apply bg-secondary text-gray-800 hover:bg-secondary/80; } + +.btn--active { + @apply shadow-none; +} \ No newline at end of file diff --git a/composer.json b/composer.json index bc3c782..b898adf 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,9 @@ "doctrine/doctrine-migrations-bundle": "^3.2", "doctrine/orm": "^2.15", "knplabs/knp-menu-bundle": "^3.2", + "liip/imagine-bundle": "^2.11", "nesbot/carbon": "^2.70", + "oneup/uploader-bundle": "^4.0", "phpdocumentor/reflection-docblock": "^5.3", "phpstan/phpdoc-parser": "^1.22", "symfony/apache-pack": "^1.0", @@ -46,6 +48,7 @@ "symfony/webpack-encore-bundle": "^2.0", "symfony/yaml": "6.3.*", "twig/extra-bundle": "^2.12|^3.0", + "twig/html-extra": "^3.7", "twig/twig": "^2.12|^3.0" }, "config": { diff --git a/composer.lock b/composer.lock index e144c34..6527102 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c7bd8e1f662fa638390820a36f96a7fd", + "content-hash": "2b10ee7954ba92748752ca9e16b1a17a", "packages": [ { "name": "doctrine/cache", @@ -1386,6 +1386,68 @@ ], "time": "2023-01-14T14:17:03+00:00" }, + { + "name": "imagine/imagine", + "version": "1.3.5", + "source": { + "type": "git", + "url": "https://github.com/php-imagine/Imagine.git", + "reference": "7151d553edec4dc2bbac60419f7a74ff34700e7f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-imagine/Imagine/zipball/7151d553edec4dc2bbac60419f7a74ff34700e7f", + "reference": "7151d553edec4dc2bbac60419f7a74ff34700e7f", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5 || ^8.4 || ^9.3" + }, + "suggest": { + "ext-exif": "to read EXIF metadata", + "ext-gd": "to use the GD implementation", + "ext-gmagick": "to use the Gmagick implementation", + "ext-imagick": "to use the Imagick implementation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-develop": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Imagine\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bulat Shakirzyanov", + "email": "mallluhuct@gmail.com", + "homepage": "http://avalanche123.com" + } + ], + "description": "Image processing for PHP 5.3", + "homepage": "http://imagine.readthedocs.org/", + "keywords": [ + "drawing", + "graphics", + "image manipulation", + "image processing" + ], + "support": { + "issues": "https://github.com/php-imagine/Imagine/issues", + "source": "https://github.com/php-imagine/Imagine/tree/1.3.5" + }, + "time": "2023-06-07T14:49:52+00:00" + }, { "name": "knplabs/knp-menu", "version": "v3.4.0", @@ -1523,6 +1585,109 @@ }, "time": "2021-10-24T07:53:34+00:00" }, + { + "name": "liip/imagine-bundle", + "version": "2.11.0", + "source": { + "type": "git", + "url": "https://github.com/liip/LiipImagineBundle.git", + "reference": "2e943e6be4309ec90fcff90227053898203c1c8e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/liip/LiipImagineBundle/zipball/2e943e6be4309ec90fcff90227053898203c1c8e", + "reference": "2e943e6be4309ec90fcff90227053898203c1c8e", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "imagine/imagine": "^1.3.2", + "php": "^7.1|^8.0", + "symfony/filesystem": "^3.4|^4.4|^5.3|^6.0", + "symfony/finder": "^3.4|^4.4|^5.3|^6.0", + "symfony/framework-bundle": "^3.4.23|^4.4|^5.3|^6.0", + "symfony/mime": "^4.4|^5.3|^6.0", + "symfony/options-resolver": "^3.4|^4.4|^5.3|^6.0", + "symfony/process": "^3.4|^4.4|^5.3|^6.0", + "twig/twig": "^1.44|^2.9|^3.0" + }, + "require-dev": { + "amazonwebservices/aws-sdk-for-php": "^1.0", + "aws/aws-sdk-php": "^2.4", + "doctrine/cache": "^1.11|^2.0", + "doctrine/persistence": "^1.3|^2.0", + "enqueue/enqueue-bundle": "^0.9|^0.10", + "ext-gd": "*", + "league/flysystem": "^1.0|^2.0|^3.0", + "phpstan/phpstan": "^0.12.64", + "psr/cache": "^1.0|^2.0|^3.0", + "psr/log": "^1.0", + "symfony/browser-kit": "^3.4|^4.4|^5.3|^6.0", + "symfony/cache": "^3.4|^4.4|^5.3|^6.0", + "symfony/console": "^3.4|^4.4|^5.3|^6.0", + "symfony/dependency-injection": "^3.4|^4.4|^5.3|^6.0", + "symfony/form": "^3.4|^4.4|^5.3|^6.0", + "symfony/messenger": "^4.4|^5.3|^6.0", + "symfony/phpunit-bridge": "^5.3", + "symfony/templating": "^3.4|^4.4|^5.3|^6.0", + "symfony/validator": "^3.4|^4.4|^5.3|^6.0", + "symfony/yaml": "^3.4|^4.4|^5.3|^6.0" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "required for mongodb components", + "amazonwebservices/aws-sdk-for-php": "required to use AWS version 1 cache resolver", + "aws/aws-sdk-php": "required to use AWS version 2/3 cache resolver", + "doctrine/mongodb-odm": "required to use mongodb-backed doctrine components", + "enqueue/enqueue-bundle": "^0.9 add if you like to process images in background", + "ext-exif": "required to read EXIF metadata from images", + "ext-gd": "required to use gd driver", + "ext-gmagick": "required to use gmagick driver", + "ext-imagick": "required to use imagick driver", + "ext-mongodb": "required for mongodb components", + "league/flysystem": "required to use FlySystem data loader or cache resolver", + "monolog/monolog": "A psr/log compatible logger is required to enable logging", + "rokka/imagine-vips": "required to use 'vips' driver", + "symfony/messenger": "If you like to process images in background", + "symfony/templating": "required to use deprecated Templating component instead of Twig" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Liip\\ImagineBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Liip and other contributors", + "homepage": "https://github.com/liip/LiipImagineBundle/contributors" + } + ], + "description": "This bundle provides an image manipulation abstraction toolkit for Symfony-based projects.", + "homepage": "http://liip.ch", + "keywords": [ + "bundle", + "image", + "imagine", + "liip", + "manipulation", + "photos", + "pictures", + "symfony", + "transformation" + ], + "support": { + "issues": "https://github.com/liip/LiipImagineBundle/issues", + "source": "https://github.com/liip/LiipImagineBundle/tree/2.11.0" + }, + "time": "2023-05-16T06:13:14+00:00" + }, { "name": "monolog/monolog", "version": "3.4.0", @@ -1730,6 +1895,100 @@ ], "time": "2023-09-07T16:43:50+00:00" }, + { + "name": "oneup/uploader-bundle", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/1up-lab/OneupUploaderBundle.git", + "reference": "34d22041d40ff45f87de56cd617a580d48cbdd8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/1up-lab/OneupUploaderBundle/zipball/34d22041d40ff45f87de56cd617a580d48cbdd8c", + "reference": "34d22041d40ff45f87de56cd617a580d48cbdd8c", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "symfony/asset": "^4.4 || ^5.4 || ^6.0", + "symfony/event-dispatcher-contracts": "^1.0 || ^2.0 || ^3.0", + "symfony/finder": "^4.4 || ^5.4 || ^6.0", + "symfony/framework-bundle": "^4.4 || ^5.4 || ^6.0", + "symfony/mime": "^4.4 || ^5.4 || ^6.0", + "symfony/templating": "^4.4 || ^5.4 || ^6.0", + "symfony/translation": "^4.4 || ^5.4 || ^6.0", + "symfony/translation-contracts": "^1.0 || ^2.0 || ^3.0", + "symfony/yaml": "^4.4 || ^5.4 || ^6.0", + "twig/twig": "^2.4 || ^3.0" + }, + "require-dev": { + "amazonwebservices/aws-sdk-for-php": "1.5.*", + "doctrine/common": "^2.12 || ^3.0", + "doctrine/doctrine-bundle": "^2.4", + "friendsofphp/php-cs-fixer": "^2.16", + "knplabs/gaufrette": "^0.9", + "m2mtech/flysystem-stream-wrapper": "^1.0", + "oneup/flysystem-bundle": "^4.1", + "phpstan/phpstan": "^0.12.10", + "phpunit/phpunit": "^9.5", + "sensio/framework-extra-bundle": "^5.0 || ^6.0", + "symfony/browser-kit": "^4.4 || ^5.4 || ^6.0", + "symfony/phpunit-bridge": "^5.4", + "symfony/security-bundle": "^4.4 || ^5.4 || ^6.0", + "symfony/var-dumper": "^4.4 || ^5.4 || ^6.0", + "twistor/flysystem-stream-wrapper": "^1.0" + }, + "suggest": { + "knplabs/knp-gaufrette-bundle": "0.1.*", + "m2mtech/flysystem-stream-wrapper": "^1.0 (Required when using Flysystem)", + "oneup/flysystem-bundle": "^3.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Oneup\\UploaderBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Schmid", + "email": "js@1up.io", + "homepage": "https://1up.io", + "role": "Developer" + }, + { + "name": "David Greminger", + "email": "dg@1up.io", + "homepage": "https://1up.io", + "role": "Developer" + } + ], + "description": "This Symfony bundle provides a server implementation for handling single and multiple file uploads using either FineUploader, jQuery File Uploader, YUI3 Uploader, Uploadify, FancyUpload, MooUpload, Plupload or Dropzone. Features include chunked uploads, orphanages, Gaufrette and Flysystem support.", + "homepage": "https://1up.io", + "keywords": [ + "FancyUpload", + "FineUploader", + "MooUpload", + "Uploadify", + "YUI3 Uploader", + "blueimp", + "dropzone", + "fileupload", + "jQuery File Uploader", + "plupload", + "upload" + ], + "support": { + "issues": "https://github.com/1up-lab/OneupUploaderBundle/issues", + "source": "https://github.com/1up-lab/OneupUploaderBundle/tree/4.0.1" + }, + "time": "2022-07-25T07:51:47+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -6654,6 +6913,71 @@ ], "time": "2023-03-21T21:06:29+00:00" }, + { + "name": "symfony/templating", + "version": "v6.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/templating.git", + "reference": "27d79475d8bcba894235b5472df158e78c6419a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/templating/zipball/27d79475d8bcba894235b5472df158e78c6419a3", + "reference": "27d79475d8bcba894235b5472df158e78c6419a3", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8" + }, + "require-dev": { + "psr/log": "^1|^2|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Templating\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides all the tools needed to build any kind of template system", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/templating/tree/v6.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-22T08:23:52+00:00" + }, { "name": "symfony/translation", "version": "v6.3.0", @@ -7644,6 +7968,70 @@ ], "time": "2023-05-06T11:11:46+00:00" }, + { + "name": "twig/html-extra", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/twigphp/html-extra.git", + "reference": "95ceb36e70fa8d07af08cf5135ecbf5e0bd8f386" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/html-extra/zipball/95ceb36e70fa8d07af08cf5135ecbf5e0bd8f386", + "reference": "95ceb36e70fa8d07af08cf5135ecbf5e0bd8f386", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/mime": "^5.4|^6.0", + "twig/twig": "^2.7|^3.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^5.4|^6.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Twig\\Extra\\Html\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + } + ], + "description": "A Twig extension for HTML", + "homepage": "https://twig.symfony.com", + "keywords": [ + "html", + "twig" + ], + "support": { + "source": "https://github.com/twigphp/html-extra/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2023-07-29T15:34:56+00:00" + }, { "name": "twig/twig", "version": "v3.6.1", diff --git a/config/bundles.php b/config/bundles.php index 1d1f8fa..f4a7e4a 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -14,4 +14,6 @@ return [ Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true], Knp\Bundle\MenuBundle\KnpMenuBundle::class => ['all' => true], + Oneup\UploaderBundle\OneupUploaderBundle::class => ['all' => true], + Liip\ImagineBundle\LiipImagineBundle::class => ['all' => true], ]; diff --git a/config/packages/liip_imagine.yaml b/config/packages/liip_imagine.yaml new file mode 100644 index 0000000..b618e5d --- /dev/null +++ b/config/packages/liip_imagine.yaml @@ -0,0 +1,25 @@ +liip_imagine: + driver: "imagick" + resolvers: + default: + web_path: ~ + cache: format_extension + loaders: + default: + filesystem: + data_root: '%kernel.project_dir%/uploads/photo' + twig: + mode: lazy + + filter_sets: + cache: ~ + thumbnail: + format: jpeg + jpeg_quality: 75 + filters: + thumbnail: { size: [ 128, 128 ], mode: outbound } + profile: + format: jpeg + jpeg_quality: 75 + filters: + thumbnail: { size: [ 128, 128 ], mode: outbound } diff --git a/config/packages/oneup_uploader.yaml b/config/packages/oneup_uploader.yaml new file mode 100644 index 0000000..a7e0d68 --- /dev/null +++ b/config/packages/oneup_uploader.yaml @@ -0,0 +1,15 @@ +oneup_uploader: + mappings: + photo: + frontend: dropzone + use_orphanage: true + namer: app.upload_namer + storage: + directory: '%kernel.project_dir%/uploads/photo' + chunks: + maxage: 86400 + storage: + directory: '%kernel.project_dir%/uploads/temp' + orphanage: + maxage: 3600 + directory: '%kernel.project_dir%/uploads/orphanage' \ No newline at end of file diff --git a/config/routes/liip_imagine.yaml b/config/routes/liip_imagine.yaml new file mode 100644 index 0000000..201cbd5 --- /dev/null +++ b/config/routes/liip_imagine.yaml @@ -0,0 +1,2 @@ +_liip_imagine: + resource: "@LiipImagineBundle/Resources/config/routing.yaml" diff --git a/config/routes/oneup_uploader.yaml b/config/routes/oneup_uploader.yaml new file mode 100644 index 0000000..ad9f64f --- /dev/null +++ b/config/routes/oneup_uploader.yaml @@ -0,0 +1,3 @@ +oneup_uploader: + resource: . + type: uploader diff --git a/config/services.yaml b/config/services.yaml index 6c47305..d4ca02a 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -19,6 +19,14 @@ services: - '../src/Entity/' - '../src/Kernel.php' + Liip\ImagineBundle\Imagine\Cache\Resolver\FormatExtensionResolver: + arguments: + - '@liip_imagine.cache.resolver.default' + - '@liip_imagine.filter.configuration' + tags: + - name: liip_imagine.cache.resolver + resolver: format_extension + App\EventListener\BlamableEntitySubscriber: tags: - { name: 'doctrine.event_subscriber', connection: 'default' } @@ -27,6 +35,13 @@ services: tags: - { name: 'doctrine.event_subscriber', connection: 'default' } + App\EventListener\LogoutListener: + tags: + - name: kernel.event_listener + event: Symfony\Component\Security\Http\Event\LogoutEvent + method: onLogout + dispatcher: security.event_dispatcher.main + App\BusProNet\ApiClient: arguments: $logger: '@monolog.logger.bpn' @@ -47,3 +62,30 @@ services: - name: knp_menu.menu_builder method: createMainMenu alias: main + + App\Service\Upload\UploadHandler: + arguments: + $orphanageManager: '@oneup_uploader.orphanage_manager' + $projectDir: '%kernel.project_dir%' + + App\EventListener\UploadSessionListener: + tags: + - name: kernel.event_listener + event: oneup_uploader.post_upload + method: onUpload + + App\EventListener\UploadSessionValidationListener: + tags: + - name: kernel.event_listener + event: oneup_uploader.validation + method: onValidate + + App\EventListener\DeleteUploadListener: + tags: + - name: doctrine.event_listener + event: preRemove + + app.upload_namer: + class: App\Service\Upload\UploadNamer + public: true + diff --git a/src/Controller/Teamer/ProfileController.php b/src/Controller/Teamer/ProfileController.php index c1eb18a..627a688 100644 --- a/src/Controller/Teamer/ProfileController.php +++ b/src/Controller/Teamer/ProfileController.php @@ -5,8 +5,10 @@ namespace App\Controller\Teamer; use App\BusProNet\ApiClient; use App\BusProNet\ApiClientException; use App\Entity\Teamer; +use App\Entity\Upload; use App\Entity\User; use App\Form\TeamerProfileType; +use App\Service\Upload\UploadHandler; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; @@ -18,7 +20,8 @@ class ProfileController extends AbstractController { public function __construct( private readonly EntityManagerInterface $entityManager, - private readonly ApiClient $apiClient + private readonly ApiClient $apiClient, + private readonly UploadHandler $uploadHandler ) { } @@ -36,10 +39,22 @@ class ProfileController extends AbstractController $this->entityManager->persist($teamer); } - $form = $this->createForm(TeamerProfileType::class, $teamer); + $uploadSession = $this->uploadHandler->getUploadSession(); + + $form = $this->createForm(TeamerProfileType::class, $teamer, ['upload_session' => $uploadSession]); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { + if ($uploadSession->getCount() > 0) { + if (null !== $existingUpload = $teamer->getPhoto()) { + $this->entityManager->remove($existingUpload); + } + $upload = Upload::fromUploadDto($uploadSession->getUploads()->first(), $user, Upload::TYPE_PHOTO); + $teamer->setPhoto($upload); + $this->uploadHandler->moveUploadSessionFilesFromOrphanage('photo', $uploadSession); + $this->uploadHandler->destroyUploadSession(); + } + try { $bpnPassword = $request->getSession()->get('bpn_password'); $this->apiClient->updateProfile($user, $bpnPassword); @@ -47,6 +62,8 @@ class ProfileController extends AbstractController } catch (ApiClientException $e) { } + $this->entityManager->flush(); + $this->addFlash('success', 'Deine Daten wurden aktualisiert'); return $this->redirectToRoute('app_teamer_profile'); @@ -54,6 +71,7 @@ class ProfileController extends AbstractController return $this->render('teamer/profile.html.twig', [ 'form' => $form->createView(), + 'teamer' => $teamer, ]); } } \ No newline at end of file diff --git a/src/Controller/Upload/DeleteController.php b/src/Controller/Upload/DeleteController.php new file mode 100644 index 0000000..26ededb --- /dev/null +++ b/src/Controller/Upload/DeleteController.php @@ -0,0 +1,30 @@ +get('uuid'); + + $this->uploadHandler->removeUploadFromSession($uuid); + + return $this->json([ + 'success' => true, + 'uuid' => $uuid, + ]); + } +} \ No newline at end of file diff --git a/src/Entity/Upload.php b/src/Entity/Upload.php index be09e56..cd150a8 100644 --- a/src/Entity/Upload.php +++ b/src/Entity/Upload.php @@ -4,6 +4,7 @@ namespace App\Entity; use App\Entity\Traits\BlameableEntity; use App\Entity\Traits\TimestampableEntity; +use App\Model\UploadDto; use App\Repository\UploadRepository; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Uid\Uuid; @@ -14,6 +15,8 @@ class Upload implements BlameableEntityInterface, TimestampableEntityInterface use BlameableEntity; use TimestampableEntity; + public const TYPE_PHOTO = 'photo'; + public const STATUS_NEW = 'new'; public const STATUS_IN_PROCESS = 'in_process'; public const STATUS_CHECKED = 'checked'; @@ -52,6 +55,21 @@ class Upload implements BlameableEntityInterface, TimestampableEntityInterface $this->uuid = Uuid::v4(); } + public static function fromUploadDto(UploadDto $uploadDto, User $owner, string $type): static + { + $instance = new static(); + $instance + ->setFilename($uploadDto->getFilename()) + ->setOriginalFilename($uploadDto->getOriginalFilename()) + ->setMimeType($uploadDto->getMimeType()) + ->setSize($uploadDto->getSize()) + ->setOwner($owner) + ->setType($type) + ; + + return $instance; + } + public function getId(): ?int { return $this->id; diff --git a/src/EventListener/DeleteUploadListener.php b/src/EventListener/DeleteUploadListener.php new file mode 100644 index 0000000..bb0533a --- /dev/null +++ b/src/EventListener/DeleteUploadListener.php @@ -0,0 +1,32 @@ +getObject(); + + if (!$object instanceof Upload) { + return; + } + + // Remove uploaded original + $this->uploadHandler->removeUploadedFile($object); + + // Remove potential variants from public folders + $this->cacheManager->remove($object->getFilename()); + } +} diff --git a/src/EventListener/LogoutListener.php b/src/EventListener/LogoutListener.php new file mode 100644 index 0000000..83e54ab --- /dev/null +++ b/src/EventListener/LogoutListener.php @@ -0,0 +1,20 @@ +authenticationLogger->info('Logout', [ + 'user' => $event->getToken()->getUserIdentifier(), + ]); + } +} diff --git a/src/EventListener/UploadSessionListener.php b/src/EventListener/UploadSessionListener.php new file mode 100644 index 0000000..97c92d9 --- /dev/null +++ b/src/EventListener/UploadSessionListener.php @@ -0,0 +1,42 @@ +getRequest(); + /** @var File $uploadedFile */ + $uploadedFile = $event->getFile(); + + // Get upload uuid from Dropzone request + $uuid = $request->get('uuid'); + + // Get original filename from Dropzone request + $originalFileName = $request->get('originalFilename'); + + $upload = new UploadDto( + $uuid, + $uploadedFile->getFilename(), + $originalFileName, + $uploadedFile->getMimeType(), + $uploadedFile->getSize() + ); + + $uploadSession = $this->uploadHandler->addUploadToSession($upload); + + $response = $event->getResponse(); + $response['uploadedTotalSize'] = $uploadSession->getSize(); + $response['uploadedTotalFiles'] = $uploadSession->getCount(); + } +} diff --git a/src/EventListener/UploadSessionValidationListener.php b/src/EventListener/UploadSessionValidationListener.php new file mode 100644 index 0000000..31bcb2f --- /dev/null +++ b/src/EventListener/UploadSessionValidationListener.php @@ -0,0 +1,33 @@ +getType()) { + return; + } + + $request = $event->getRequest(); + $session = $request->getSession(); + $uploadSessionId = $request->get(UploadHandler::SESSION_KEY); + + if (null === $uploadSessionId || false === $session->has(UploadHandler::SESSION_KEY)) { + throw new ValidationException('Invalid upload session'); + } + + /** @var UploadSessionDto $uploadSession */ + $uploadSession = $session->get(UploadHandler::SESSION_KEY); + + if (!$uploadSession instanceof UploadSessionDto || $uploadSession->getUid() !== $uploadSessionId) { + throw new ValidationException('Invalid upload session'); + } + } +} diff --git a/src/Form/TeamerProfileType.php b/src/Form/TeamerProfileType.php index 33da3d5..2a2ff7f 100644 --- a/src/Form/TeamerProfileType.php +++ b/src/Form/TeamerProfileType.php @@ -3,12 +3,16 @@ namespace App\Form; use App\Entity\Teamer; +use App\Model\UploadSessionDto; +use App\Service\Upload\UploadHandler; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\BirthdayType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; use Symfony\Component\OptionsResolver\OptionsResolver; class TeamerProfileType extends AbstractType @@ -77,10 +81,23 @@ class TeamerProfileType extends AbstractType ; } + public function buildView(FormView $view, FormInterface $form, array $options): void + { + /** @var UploadSessionDto $uploadSession */ + $uploadSession = $options['upload_session']; + $view->vars['upload_session_params'] = [ + UploadHandler::SESSION_KEY => $uploadSession->getUid(), + ]; + } + public function configureOptions(OptionsResolver $resolver): void { - $resolver->setDefaults([ - 'data_class' => Teamer::class, - ]); + $resolver + ->setDefaults([ + 'data_class' => Teamer::class, + ]) + ->setRequired(['upload_session']) + ->setAllowedTypes('upload_session', UploadSessionDto::class) + ; } } \ No newline at end of file diff --git a/src/Model/UploadDto.php b/src/Model/UploadDto.php new file mode 100644 index 0000000..e16d34e --- /dev/null +++ b/src/Model/UploadDto.php @@ -0,0 +1,51 @@ +uuid = $uuid; + $this->filename = $filename; + $this->originalFilename = $originalFilename; + $this->mimeType = $mimeType; + $this->size = $size; + } + + public function getUuid(): string + { + return $this->uuid; + } + + public function getFilename(): string + { + return $this->filename; + } + + public function getOriginalFilename(): string + { + return $this->originalFilename; + } + + public function getMimeType(): string + { + return $this->mimeType; + } + + public function getSize(): int + { + return $this->size; + } +} diff --git a/src/Model/UploadSessionDto.php b/src/Model/UploadSessionDto.php new file mode 100644 index 0000000..f23c7d5 --- /dev/null +++ b/src/Model/UploadSessionDto.php @@ -0,0 +1,77 @@ +uid = $uid ?? Uuid::v4(); + $this->uploads = new ArrayCollection(); + } + + public function getUid(): string + { + return $this->uid; + } + + public function addUpload(UploadDto $upload): static + { + if (false === $this->uploads->contains($upload)) { + $this->uploads[] = $upload; + } + + return $this; + } + + public function removeUpload(string $uuid): static + { + $uploadToRemove = $this->uploads->filter(function (UploadDto $upload) use ($uuid) { + return $uuid === $upload->getUuid(); + })->first(); + + if (null !== $uploadToRemove) { + $this->uploads->removeElement($uploadToRemove); + } + + return $this; + } + + public function getUploads(): Collection + { + return $this->uploads; + } + + public function flushUploads(): static + { + $this->uploads->clear(); + + return $this; + } + + public function getSize(): int + { + $size = 0; + + foreach ($this->uploads as $upload) { + $size += $upload->getSize(); + } + + return $size; + } + + public function getCount(): int + { + return $this->uploads->count(); + } +} diff --git a/src/Service/Upload/UploadHandler.php b/src/Service/Upload/UploadHandler.php new file mode 100644 index 0000000..2ffa176 --- /dev/null +++ b/src/Service/Upload/UploadHandler.php @@ -0,0 +1,134 @@ +requestStack->getSession(); + + if ($session->has(self::SESSION_KEY) && null !== $session->get(self::SESSION_KEY)) { + /** @var UploadSessionDto $uploadSession */ + $uploadSession = $session->get(self::SESSION_KEY); + } else { + $uploadSession = new UploadSessionDto(); + $session->set(self::SESSION_KEY, $uploadSession); + } + + return $uploadSession; + } + + public function destroyUploadSession(): void + { + $this->requestStack->getSession()->remove(self::SESSION_KEY); + } + + public function flushUploadSession(UploadSessionDto $uploadSession): void + { + $uploadSession->flushUploads(); + $session = $this->requestStack->getSession(); + $session->set(self::SESSION_KEY, $uploadSession); + } + + public function addUploadToSession(UploadDto $upload): UploadSessionDto + { + $uploadSession = $this->getUploadSession(); + $uploadSession->addUpload($upload); + + $session = $this->requestStack->getSession(); + $session->set(self::SESSION_KEY, $uploadSession); + + return $uploadSession; + } + + public function removeUploadFromSession(string $filename): UploadSessionDto + { + $uploadSession = $this->getUploadSession(); + $uploadSession->removeUpload($filename); + + $session = $this->requestStack->getSession(); + $session->set(self::SESSION_KEY, $uploadSession); + + return $uploadSession; + } + + public function moveUploadSessionFilesFromOrphanage(string $uploader, UploadSessionDto $uploadSession): void + { + $uploadSessionFilenames = $this->getUploadSessionFilenames($uploadSession, true); + $this->moveUploads($uploader, $uploadSessionFilenames); + } + + public function moveUploads(string $uploader, array $filenames = []): array + { + $manager = $this->orphanageManager->get($uploader); + + if (0 === count($filenames)) { + return $manager->uploadFiles(); + } + + $files = $manager->getFiles(); + $files->files()->name($filenames); + + return $manager->uploadFiles(iterator_to_array($files)); + } + + public function removeUploadedFile(Upload $upload): void + { + $filepath = $this->getUploadFilepath($upload); + + $filesystem = new Filesystem(); + + try { + $filesystem->remove($filepath); + } catch (IOException $e) { + } + } + + public function getUploadSessionFilenames(UploadSessionDto $uploadSession, bool $basename = false): array + { + $uploadSessionId = $uploadSession->getUid(); + + return $uploadSession + ->getUploads() + ->map(function (UploadDto $uploadDto) use ($basename, $uploadSessionId) { + return true === $basename ? $uploadDto->getFilename() : $this->getTempUploadFilepath($uploadDto, $uploadSessionId); + }) + ->toArray() + ; + } + + public function getUploadFilepath(Upload $upload): string + { + return sprintf('%s/uploads/%s/%s', $this->projectDir, $upload->getType(), $upload->getFilename()); + } + + public function getTempUploadFilepath(UploadDto $uploadDto, string $uploadSessionId): string + { + $tempUploadDir = $this->getTempUploadDir($uploadSessionId); + + return sprintf('%s/%s', $tempUploadDir, $uploadDto->getFilename()); + } + + public function getTempUploadDir(string $uploadSessionId): string + { + return sprintf('%s/uploads/temp/%s', $this->projectDir, $uploadSessionId); + } +} diff --git a/src/Service/Upload/UploadNamer.php b/src/Service/Upload/UploadNamer.php new file mode 100644 index 0000000..750a1fa --- /dev/null +++ b/src/Service/Upload/UploadNamer.php @@ -0,0 +1,15 @@ +getExtension())); + } +} \ No newline at end of file diff --git a/symfony.lock b/symfony.lock index 85b2ba6..c7b0436 100644 --- a/symfony.lock +++ b/symfony.lock @@ -29,6 +29,32 @@ "knplabs/knp-menu-bundle": { "version": "v3.2.0" }, + "liip/imagine-bundle": { + "version": "2.11", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.8", + "ref": "d1227d002b70d1a1f941d91845fcd7ac7fbfc929" + }, + "files": [ + "config/packages/liip_imagine.yaml", + "config/routes/liip_imagine.yaml" + ] + }, + "oneup/uploader-bundle": { + "version": "4.0", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "2.0", + "ref": "b19678d1535b9fe7cdf4647fd3483455e4fca458" + }, + "files": [ + "config/packages/oneup_uploader.yaml", + "config/routes/oneup_uploader.yaml" + ] + }, "phpunit/phpunit": { "version": "9.6", "recipe": { diff --git a/templates/_partials/_upload_collection_form.html.twig b/templates/_partials/_upload_collection_form.html.twig new file mode 100644 index 0000000..96f987b --- /dev/null +++ b/templates/_partials/_upload_collection_form.html.twig @@ -0,0 +1,16 @@ +
+
+
diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig index 3868058..12627c9 100644 --- a/templates/security/login.html.twig +++ b/templates/security/login.html.twig @@ -16,18 +16,18 @@ {% endif %}
-
-
- diff --git a/templates/security/password_reset.html.twig b/templates/security/password_reset.html.twig index 7443797..b4cf6ce 100644 --- a/templates/security/password_reset.html.twig +++ b/templates/security/password_reset.html.twig @@ -14,7 +14,7 @@ {{ form_widget(form.email) }} {{ form_errors(form.email) }} - {{ form_rest(form) }} diff --git a/templates/teamer/profile.html.twig b/templates/teamer/profile.html.twig index 20398b5..c9e4626 100644 --- a/templates/teamer/profile.html.twig +++ b/templates/teamer/profile.html.twig @@ -17,7 +17,7 @@ {% block content %} {{ form_start(form) }} -
+
@@ -31,7 +31,7 @@ Bankverbindung
@@ -94,11 +94,33 @@

Sonstiges

-
+
{{ form_row(form.taxId) }} {{ form_row(form.healthInsuranceCompany) }} {{ form_row(form.remarks) }}
+

+ Fotoupload +

+
+
+ {% include '_partials/_upload_collection_form.html.twig' with { + 'endpoint_upload': path('_uploader_upload_photo'), + 'upload_session_params': form.vars.upload_session_params, + } %} +
+
+ {% if teamer.photo %} + {{ teamer.firstName }} + {% else %} + + + + {% endif %} +
+
diff --git a/uploads/orphanage/9c86f2dad648aa53dc7f5cd9f3d13e16/photo/65086541143a5.jpg b/uploads/orphanage/9c86f2dad648aa53dc7f5cd9f3d13e16/photo/65086541143a5.jpg new file mode 100644 index 0000000..23f00f6 Binary files /dev/null and b/uploads/orphanage/9c86f2dad648aa53dc7f5cd9f3d13e16/photo/65086541143a5.jpg differ diff --git a/uploads/orphanage/9c86f2dad648aa53dc7f5cd9f3d13e16/upload_collection/65278ee0-e8ce-4183-91c3-42cb5319c96b/650856b7357b5 b/uploads/orphanage/9c86f2dad648aa53dc7f5cd9f3d13e16/upload_collection/65278ee0-e8ce-4183-91c3-42cb5319c96b/650856b7357b5 new file mode 100644 index 0000000..ca06cb3 Binary files /dev/null and b/uploads/orphanage/9c86f2dad648aa53dc7f5cd9f3d13e16/upload_collection/65278ee0-e8ce-4183-91c3-42cb5319c96b/650856b7357b5 differ diff --git a/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/6508478d63b7a b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/6508478d63b7a new file mode 100644 index 0000000..bdfc6b2 Binary files /dev/null and b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/6508478d63b7a differ diff --git a/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/650847f51c68f b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/650847f51c68f new file mode 100644 index 0000000..bdfc6b2 Binary files /dev/null and b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/650847f51c68f differ diff --git a/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084c0d7bf21 b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084c0d7bf21 new file mode 100644 index 0000000..23ae1f6 Binary files /dev/null and b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084c0d7bf21 differ diff --git a/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084c984f325 b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084c984f325 new file mode 100644 index 0000000..23f00f6 Binary files /dev/null and b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084c984f325 differ diff --git a/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084caa12ebc b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084caa12ebc new file mode 100644 index 0000000..23ae1f6 Binary files /dev/null and b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084caa12ebc differ diff --git a/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084caa8fcc2 b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084caa8fcc2 new file mode 100644 index 0000000..99a8559 Binary files /dev/null and b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084caa8fcc2 differ diff --git a/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084e7869046 b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084e7869046 new file mode 100644 index 0000000..ca06cb3 Binary files /dev/null and b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084e7869046 differ diff --git a/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084e7870be4 b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084e7870be4 new file mode 100644 index 0000000..d1db493 Binary files /dev/null and b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084e7870be4 differ diff --git a/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084e796ac53 b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084e796ac53 new file mode 100644 index 0000000..b736e56 Binary files /dev/null and b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084e796ac53 differ diff --git a/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084ee9aff91 b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084ee9aff91 new file mode 100644 index 0000000..23ae1f6 Binary files /dev/null and b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084ee9aff91 differ diff --git a/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084f4a073a3 b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084f4a073a3 new file mode 100644 index 0000000..23ae1f6 Binary files /dev/null and b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084f4a073a3 differ diff --git a/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084ffd32311 b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084ffd32311 new file mode 100644 index 0000000..23ae1f6 Binary files /dev/null and b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084ffd32311 differ diff --git a/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084ffd39cff b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084ffd39cff new file mode 100644 index 0000000..91e1665 Binary files /dev/null and b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084ffd39cff differ diff --git a/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084ffd4405e b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084ffd4405e new file mode 100644 index 0000000..23f00f6 Binary files /dev/null and b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65084ffd4405e differ diff --git a/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/650854210c7c8 b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/650854210c7c8 new file mode 100644 index 0000000..ca06cb3 Binary files /dev/null and b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/650854210c7c8 differ diff --git a/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65085479aac92 b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65085479aac92 new file mode 100644 index 0000000..ca06cb3 Binary files /dev/null and b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/65085479aac92 differ diff --git a/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/6508559fd2b7a b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/6508559fd2b7a new file mode 100644 index 0000000..ca06cb3 Binary files /dev/null and b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/6508559fd2b7a differ diff --git a/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/6508562a1b768 b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/6508562a1b768 new file mode 100644 index 0000000..ca06cb3 Binary files /dev/null and b/uploads/orphanage/a20031d0b0e9509b1a2bf05434c37ac4/upload_collection/260f3c50-b4a9-4eb1-b869-ff387353c638/6508562a1b768 differ diff --git a/uploads/photo/beeba7df-1c5f-4294-b5b8-f5d3d658e15d.jpg b/uploads/photo/beeba7df-1c5f-4294-b5b8-f5d3d658e15d.jpg new file mode 100644 index 0000000..461751c Binary files /dev/null and b/uploads/photo/beeba7df-1c5f-4294-b5b8-f5d3d658e15d.jpg differ