Compare commits

...

18 Commits

Author SHA1 Message Date
tracer c09249bcbe Merge pull request 'turbo' (#4) from turbo into master
Reviewed-on: #4
2022-11-15 17:19:49 +01:00
tracer 27454709af before remerve from turbo to master 2022-11-15 17:17:47 +01:00
tracer cd5f4e6590 disabled UX-Dropzone 2022-11-15 17:14:43 +01:00
tracer 995f47a888 added tmpAvatar 2022-11-15 17:13:03 +01:00
tracer a64fa311a0 added result type to resendVerifyEmail 2022-11-15 17:12:37 +01:00
tracer 763e3f4918 small cli tool to purge unused avatar uploads 2022-11-15 17:11:25 +01:00
tracer f48529b743 working version of edit profile 2022-11-15 17:10:54 +01:00
tracer 481849b530 refactored from render to renderForm 2022-11-15 17:08:44 +01:00
tracer f1a8821ba7 some bundles were added 2022-11-15 17:07:58 +01:00
tracer d8aca18192 changed size of small 2022-11-15 17:07:27 +01:00
tracer 28d77f7ec5 new controllers added by bundles 2022-11-15 17:05:12 +01:00
tracer 5247d71050 refactored dropzone stuff into controller 2022-11-15 17:04:31 +01:00
tracer 42a4fc1d8a dropzone styling 2022-11-15 17:02:20 +01:00
tracer 4deaf39e89 symlinked 2022-11-15 16:59:31 +01:00
tracer 3a322a7074 symlinked 2022-11-15 16:43:20 +01:00
tracer d9b86de4bb symlinked default 2022-11-15 16:39:18 +01:00
tracer 1d783f0cc6 symlinked default 2022-11-15 16:34:59 +01:00
tracer 5ee0e85903 symlinked 2022-11-15 16:31:06 +01:00
39 changed files with 1600 additions and 241 deletions

View File

@ -11,57 +11,24 @@ import './styles/app.scss'
import $ from 'jquery'
import 'bootstrap'
// Dropzone stuff move to component
import {Dropzone} from 'dropzone'
// TODO handle error (Chapter 26)
const formElement = $('#dropzoneForm')
if (formElement) {
const previewContent = $('#preview-content').html()
console.log(previewContent)
const dropzone = new Dropzone('#dropzoneForm', {
acceptedFiles: '.jpg, .jpeg, .png',
maxFiles: 1,
init: function () {
this.hiddenFileInput.removeAttribute('multiple')
this.on('maxfilesexceeded', (file) => {
this.removeAllFiles()
this.addFile(file)
})
this.on('error', (file, data) => {
console.log('error');
if (data.detail) {
this.emit('error', file, data.detail)
}
})
}
})
console.log('filename', previewContent)
const mockFile = { name: previewContent }
console.log('file', mockFile)
dropzone.displayExistingFile(mockFile)
}
// End Dropzone stuff move to component
// start the Stimulus application
import './bootstrap'
//import './js/index'
// needed for legacy code
//global.$ = $
if (window.matchMedia('(prefers-color-scheme)').media !== 'not all') {
console.log('🎉 Dark mode is supported')
console.log('🎉 Dark mode is supported')
}
$(document).ready(() => {
console.log('ready')
$('#toggleSidebar').on('click', () => {
const toggleIcon = $('#toggleIcon')
toggleIcon.toggleClass('fa fa-lg fa-fw fa-caret-square-o-left')
toggleIcon.toggleClass('fa fa-lg fa-fw fa-caret-square-o-right')
$('#sidebar').toggleClass('active')
})
console.log('ready')
$('#toggleSidebar').on('click', () => {
const toggleIcon = $('#toggleIcon')
toggleIcon.toggleClass('fa fa-lg fa-fw fa-caret-square-o-left')
toggleIcon.toggleClass('fa fa-lg fa-fw fa-caret-square-o-right')
$('#sidebar').toggleClass('active')
})
})
// start the Stimulus application
//import './bootstrap'

View File

@ -1,4 +1,30 @@
{
"controllers": [],
"controllers": {
"@symfony/ux-cropperjs": {
"cropper": {
"enabled": true,
"fetch": "eager",
"autoimport": {
"cropperjs/dist/cropper.min.css": true,
"@symfony/ux-cropperjs/src/style.css": true
}
}
},
"@symfony/ux-dropzone": {
"dropzone": {
"enabled": true,
"fetch": "eager",
"autoimport": {
"@symfony/ux-dropzone/src/style.css": true
}
}
},
"@symfony/ux-turbo": {
"turbo-core": {
"enabled": true,
"fetch": "eager"
}
}
},
"entrypoints": []
}

View File

@ -1,16 +0,0 @@
import { Controller } from '@hotwired/stimulus';
/*
* This is an example Stimulus controller!
*
* Any element with a data-controller="hello" attribute will cause
* this controller to be executed. The name "hello" comes from the filename:
* hello_controller.js -> "hello"
*
* Delete this file or adapt it for your use!
*/
export default class extends Controller {
connect() {
this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js';
}
}

View File

@ -0,0 +1,125 @@
import { Controller } from '@hotwired/stimulus'
import { Dropzone } from 'dropzone'
import Cropper from 'cropperjs'
export default class extends Controller {
static values = {
avatarImage: String,
userId: Number
}
connect() {
let avatarDropzone = new Dropzone('#avatarDropzone', {
url: `/user/upload/avatar/${this.userIdValue}`,
avatarUrl: this.avatarImageValue,
acceptedFiles: '.jpg, .jpeg, .png, gif',
maxFiles: 1,
dictDefaultMessage: '',
init() {
avatarDropzone = this
// If the thumbnail is already in the right size on your server:
const mockFile = { name: 'Filename', size: 12345 }
const callback = null // Optional callback when it's done
const crossOrigin = null // Added to the `img` tag for crossOrigin handling
const resizeThumbnail = false // Tells Dropzone whether it should resize the image first
avatarDropzone.displayExistingFile(
mockFile,
avatarDropzone.options.avatarUrl,
callback,
crossOrigin,
resizeThumbnail
)
avatarDropzone.files.push(mockFile) // line missing in official docs
},
transformFile(file, done) {
// Create the image editor overlay
const editor = document.createElement('div')
editor.style.position = 'fixed'
editor.style.left = '25px'
editor.style.right = '25px'
editor.style.top = '25px'
editor.style.bottom = '25px'
editor.style.zIndex = '9999'
editor.style.borderRadius = '15px;'
editor.style.borderColor = '#FF8844'
editor.style.backgroundColor = '#000'
document.body.appendChild(editor)
// Create an image node for Cropper.js
const image = new Image()
image.src = URL.createObjectURL(file)
editor.appendChild(image)
// Create Cropper.js
const cropper = new Cropper(image, { aspectRatio: 1 })
// Create confirm button at the top left of the viewport
const buttonCrop = document.createElement('button')
buttonCrop.style.position = 'absolute'
buttonCrop.style.right = '10px'
buttonCrop.style.bottom = '50px'
buttonCrop.style.zIndex = '9999'
buttonCrop.textContent = 'Crop'
buttonCrop.className = 'btn btn-secondary'
editor.appendChild(buttonCrop)
buttonCrop.addEventListener('click', () => {
// clear preview
//formElement.style.backgroundImage = ''
const canvas = cropper.getCroppedCanvas({
width: 256,
height: 256
})
canvas.toBlob((blob) => {
avatarDropzone.createThumbnail(
blob,
avatarDropzone.options.thumbnailWidth,
avatarDropzone.options.thumbnailHeight,
avatarDropzone.options.thumbnailMethod,
false,
(dataURL) => {
avatarDropzone.emit('thumbnail', file, dataURL)
done(blob)
}
)
})
// Remove the editor from the view
document.body.removeChild(editor)
})
// Create keep original button
const buttonKeep = document.createElement('button')
buttonKeep.style.position = 'absolute'
buttonKeep.style.right = '100px'
buttonKeep.style.bottom = '50px'
buttonKeep.style.zIndex = '9999'
buttonKeep.textContent = 'Keep Original'
buttonKeep.className = 'btn btn-primary'
buttonKeep.focus()
editor.appendChild(buttonKeep)
buttonKeep.addEventListener('click', () => {
//formElement.style.backgroundImage = ''
// Remove the editor from the view
this.removeAllFiles()
done(file)
document.body.removeChild(editor)
})
// info text
const infoText = document.createElement('span')
infoText.style.position = 'absolute'
infoText.style.opacity = '0.5'
infoText.style.left = '100px'
infoText.style.bottom = '50px'
infoText.style.display = 'inline:block'
infoText.style.zIndex = '9999'
infoText.textContent = 'Use mouse wheel/touchpad to adapt size'
infoText.className = 'btn btn-secondary'
editor.appendChild(infoText)
}
})
}
}

View File

@ -27,9 +27,9 @@ $list-group-hover-bg: darken($list-group, 10%);
$list-group-active-bg: $list-group;
$list-group-action-active-bg: $list-group;
@import 'bootstrap/scss/bootstrap';
@import '~bootstrap';
@import 'dropzone/dist/dropzone';
@import 'cropperjs';
@import './components/sidebar';
@ -40,19 +40,35 @@ html, body {
background: #0e0e10;
}
// Dropzone
.dropzone .dz-preview.dz-image-preview {
// Dropzone
.dropzone .dz-preview {
background: transparent !important;
margin: 0 !important;
}
.dropzone-container {
width: 160px;
height: 160px;
}
.dropzone-preview-filename {
word-wrap: anywhere;
display: none;
}
.dropzone-preview-image {
flex-basis: 0;
min-width: 150px;
max-width: 150px;
min-height: 150px;
max-height: 150px;
}
.dz-image {
background: transparent !important;
}
.article-author-img {
width: 80px;
margin: 15px;
}
///
#main_content {
overflow-y: auto;

View File

@ -16,6 +16,8 @@
"league/commonmark": "^2.3",
"liip/imagine-bundle": "^2.9",
"nelmio/cors-bundle": "^2.2",
"php-flasher/flasher-sweetalert-symfony": "^1.7",
"php-flasher/flasher-symfony": "^1.7",
"phpdocumentor/reflection-docblock": "^5.3",
"phpstan/phpdoc-parser": "^1.4",
"rector/rector": "^0.12.21",
@ -48,6 +50,9 @@
"symfony/string": "6.1.*",
"symfony/translation": "6.1.*",
"symfony/twig-bundle": "6.1.*",
"symfony/ux-cropperjs": "^2.5",
"symfony/ux-dropzone": "^2.5",
"symfony/ux-turbo": "^2.5",
"symfony/validator": "6.1.*",
"symfony/web-link": "6.1.*",
"symfony/webapp-meta": "^1.0",
@ -57,7 +62,8 @@
"symfonycasts/verify-email-bundle": "^1.12",
"twig/extra-bundle": "^3.4",
"twig/markdown-extra": "^3.4",
"twig/twig": "3.4.*"
"twig/twig": "3.4.*",
"ext-http": "*"
},
"config": {
"allow-plugins": {

930
composer.lock generated
View File

@ -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": "1c940d128112ec366d0f81bc6eccee45",
"content-hash": "e475387f74f1428bb35d9943950aa2f8",
"packages": [
{
"name": "dflydev/dot-access-data",
@ -1689,6 +1689,125 @@
],
"time": "2022-10-17T19:48:16+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "2.4.3",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "67c26b443f348a51926030c83481b85718457d3d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/67c26b443f348a51926030c83481b85718457d3d",
"reference": "67c26b443f348a51926030c83481b85718457d3d",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.0",
"ralouphie/getallheaders": "^3.0"
},
"provide": {
"psr/http-factory-implementation": "1.0",
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.1",
"http-interop/http-factory-tests": "^0.9",
"phpunit/phpunit": "^8.5.29 || ^9.5.23"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "2.4-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Psr7\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "George Mponos",
"email": "gmponos@gmail.com",
"homepage": "https://github.com/gmponos"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://github.com/sagikazarmark"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://sagikazarmark.hu"
}
],
"description": "PSR-7 message implementation that also provides common utility methods",
"keywords": [
"http",
"message",
"psr-7",
"request",
"response",
"stream",
"uri",
"url"
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.4.3"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
"type": "tidelift"
}
],
"time": "2022-10-26T14:07:24+00:00"
},
{
"name": "imagine/imagine",
"version": "1.3.2",
@ -1751,6 +1870,90 @@
},
"time": "2022-04-01T11:58:30+00:00"
},
{
"name": "intervention/image",
"version": "2.7.2",
"source": {
"type": "git",
"url": "https://github.com/Intervention/image.git",
"reference": "04be355f8d6734c826045d02a1079ad658322dad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Intervention/image/zipball/04be355f8d6734c826045d02a1079ad658322dad",
"reference": "04be355f8d6734c826045d02a1079ad658322dad",
"shasum": ""
},
"require": {
"ext-fileinfo": "*",
"guzzlehttp/psr7": "~1.1 || ^2.0",
"php": ">=5.4.0"
},
"require-dev": {
"mockery/mockery": "~0.9.2",
"phpunit/phpunit": "^4.8 || ^5.7 || ^7.5.15"
},
"suggest": {
"ext-gd": "to use GD library based image processing.",
"ext-imagick": "to use Imagick based image processing.",
"intervention/imagecache": "Caching extension for the Intervention Image library"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.4-dev"
},
"laravel": {
"providers": [
"Intervention\\Image\\ImageServiceProvider"
],
"aliases": {
"Image": "Intervention\\Image\\Facades\\Image"
}
}
},
"autoload": {
"psr-4": {
"Intervention\\Image\\": "src/Intervention/Image"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Oliver Vogel",
"email": "oliver@intervention.io",
"homepage": "https://intervention.io/"
}
],
"description": "Image handling and manipulation library with support for Laravel integration",
"homepage": "http://image.intervention.io/",
"keywords": [
"gd",
"image",
"imagick",
"laravel",
"thumbnail",
"watermark"
],
"support": {
"issues": "https://github.com/Intervention/image/issues",
"source": "https://github.com/Intervention/image/tree/2.7.2"
},
"funding": [
{
"url": "https://paypal.me/interventionio",
"type": "custom"
},
{
"url": "https://github.com/Intervention",
"type": "github"
}
],
"time": "2022-05-21T17:30:32+00:00"
},
{
"name": "knplabs/knp-time-bundle",
"version": "v1.20.0",
@ -2489,6 +2692,316 @@
},
"time": "2022-09-12T23:36:20+00:00"
},
{
"name": "php-flasher/flasher",
"version": "v1.7.0",
"source": {
"type": "git",
"url": "https://github.com/php-flasher/flasher.git",
"reference": "10071abc73d0311a99e1860e88d5895ac4d7535c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-flasher/flasher/zipball/10071abc73d0311a99e1860e88d5895ac4d7535c",
"reference": "10071abc73d0311a99e1860e88d5895ac4d7535c",
"shasum": ""
},
"require": {
"php": ">=5.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Flasher\\Prime\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Younes KHOUBZA",
"email": "younes.khoubza@gmail.com",
"homepage": "https://www.linkedin.com/in/younes-khoubza",
"role": "Developer"
}
],
"description": "A powerful and flexible flash notification system for PHP",
"homepage": "https://php-flasher.io",
"keywords": [
"Flasher",
"alerts",
"flash",
"laravel",
"lumen",
"messages",
"notifications",
"notify",
"noty",
"notyf",
"php",
"pnotify",
"sweet alert",
"symfony",
"toastr"
],
"support": {
"source": "https://github.com/php-flasher/flasher/tree/v1.7.0"
},
"funding": [
{
"url": "https://www.paypal.com/paypalme/yoeunes",
"type": "custom"
},
{
"url": "https://ko-fi.com/yoeunes",
"type": "ko_fi"
},
{
"url": "https://opencollective.com/php-flasher",
"type": "open_collective"
},
{
"url": "https://www.patreon.com/yoeunes",
"type": "patreon"
}
],
"time": "2022-07-05T19:33:26+00:00"
},
{
"name": "php-flasher/flasher-sweetalert",
"version": "v1.7.0",
"source": {
"type": "git",
"url": "https://github.com/php-flasher/flasher-sweetalert.git",
"reference": "abab36c6a70405ad9838238338a76853541baa34"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-flasher/flasher-sweetalert/zipball/abab36c6a70405ad9838238338a76853541baa34",
"reference": "abab36c6a70405ad9838238338a76853541baa34",
"shasum": ""
},
"require": {
"php": ">=5.3",
"php-flasher/flasher": "^1.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Flasher\\SweetAlert\\Prime\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Younes KHOUBZA",
"email": "younes.khoubza@gmail.com",
"homepage": "https://www.linkedin.com/in/younes-khoubza",
"role": "Developer"
}
],
"description": "PHP Flasher adapter for Sweet Alert 2",
"homepage": "https://php-flasher.io",
"keywords": [
"alerts",
"laravel",
"lumen",
"messages",
"notifications",
"notify",
"php",
"pnotify",
"sweetalert",
"symfony"
],
"support": {
"source": "https://github.com/php-flasher/flasher-sweetalert/tree/v1.7.0"
},
"funding": [
{
"url": "https://www.paypal.com/paypalme/yoeunes",
"type": "custom"
},
{
"url": "https://ko-fi.com/yoeunes",
"type": "ko_fi"
},
{
"url": "https://opencollective.com/php-flasher",
"type": "open_collective"
},
{
"url": "https://www.patreon.com/yoeunes",
"type": "patreon"
}
],
"time": "2022-07-05T19:36:36+00:00"
},
{
"name": "php-flasher/flasher-sweetalert-symfony",
"version": "v1.7.0",
"source": {
"type": "git",
"url": "https://github.com/php-flasher/flasher-sweetalert-symfony.git",
"reference": "f8f10ad9760b30c8fc1fe4e9891b595a5cd8c51e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-flasher/flasher-sweetalert-symfony/zipball/f8f10ad9760b30c8fc1fe4e9891b595a5cd8c51e",
"reference": "f8f10ad9760b30c8fc1fe4e9891b595a5cd8c51e",
"shasum": ""
},
"require": {
"php": ">=5.3",
"php-flasher/flasher-sweetalert": "^1.7",
"php-flasher/flasher-symfony": "^1.7"
},
"type": "symfony-bundle",
"autoload": {
"psr-4": {
"Flasher\\SweetAlert\\Symfony\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Younes KHOUBZA",
"email": "younes.khoubza@gmail.com",
"homepage": "https://www.linkedin.com/in/younes-khoubza",
"role": "Developer"
}
],
"description": "PHP Flasher Symfony adapter for Sweet Alert 2",
"homepage": "https://php-flasher.io",
"keywords": [
"alerts",
"bundle",
"flex",
"laravel",
"lumen",
"messages",
"notifications",
"notify",
"php",
"pnotify",
"sweetalert",
"symfony",
"toastr",
"yoeunes"
],
"support": {
"source": "https://github.com/php-flasher/flasher-sweetalert-symfony/tree/v1.7.0"
},
"funding": [
{
"url": "https://www.paypal.com/paypalme/yoeunes",
"type": "custom"
},
{
"url": "https://ko-fi.com/yoeunes",
"type": "ko_fi"
},
{
"url": "https://opencollective.com/php-flasher",
"type": "open_collective"
},
{
"url": "https://www.patreon.com/yoeunes",
"type": "patreon"
}
],
"time": "2022-07-05T19:36:36+00:00"
},
{
"name": "php-flasher/flasher-symfony",
"version": "v1.7.0",
"source": {
"type": "git",
"url": "https://github.com/php-flasher/flasher-symfony.git",
"reference": "1e003819402f415d07b8fedabfff527bd164822b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-flasher/flasher-symfony/zipball/1e003819402f415d07b8fedabfff527bd164822b",
"reference": "1e003819402f415d07b8fedabfff527bd164822b",
"shasum": ""
},
"require": {
"php": ">=5.3",
"php-flasher/flasher": "^1.7",
"symfony/config": "^2.0|^3.0|^4.0|^5.0|^6.0",
"symfony/console": "^2.0|^3.0|^4.0|^5.0|^6.0",
"symfony/dependency-injection": "^2.0|^3.0|^4.0|^5.0|^6.0",
"symfony/http-kernel": "^2.0|^3.0|^4.0|^5.0|^6.0",
"symfony/translation": "^2.0|^3.0|^4.0|^5.0|^6.0",
"symfony/twig-bundle": "^2.0|^3.0|^4.0|^5.0|^6.0"
},
"type": "symfony-bundle",
"autoload": {
"psr-4": {
"Flasher\\Symfony\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Younes KHOUBZA",
"email": "younes.khoubza@gmail.com",
"homepage": "https://www.linkedin.com/in/younes-khoubza",
"role": "Developer"
}
],
"description": "Symfony bundle integrating PHP Flasher into Symfony applications",
"homepage": "https://php-flasher.io",
"keywords": [
"alerts",
"bundle",
"flex",
"laravel",
"lumen",
"messages",
"notifications",
"notify",
"php",
"pnotify",
"symfony",
"toastr",
"yoeunes"
],
"support": {
"source": "https://github.com/php-flasher/flasher-symfony/tree/v1.7.0"
},
"funding": [
{
"url": "https://www.paypal.com/paypalme/yoeunes",
"type": "custom"
},
{
"url": "https://ko-fi.com/yoeunes",
"type": "ko_fi"
},
{
"url": "https://opencollective.com/php-flasher",
"type": "open_collective"
},
{
"url": "https://www.patreon.com/yoeunes",
"type": "patreon"
}
],
"time": "2022-07-05T19:36:36+00:00"
},
{
"name": "phpdocumentor/reflection-common",
"version": "2.2.0",
@ -2910,6 +3423,114 @@
},
"time": "2019-01-08T18:20:26+00:00"
},
{
"name": "psr/http-factory",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-factory.git",
"reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be",
"reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be",
"shasum": ""
},
"require": {
"php": ">=7.0.0",
"psr/http-message": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interfaces for PSR-7 HTTP message factories",
"keywords": [
"factory",
"http",
"message",
"psr",
"psr-17",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-factory/tree/master"
},
"time": "2019-04-30T12:38:16+00:00"
},
{
"name": "psr/http-message",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
"homepage": "https://github.com/php-fig/http-message",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-message/tree/master"
},
"time": "2016-08-06T14:39:51+00:00"
},
{
"name": "psr/link",
"version": "2.0.1",
@ -3016,6 +3637,50 @@
},
"time": "2021-07-14T16:46:02+00:00"
},
{
"name": "ralouphie/getallheaders",
"version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/ralouphie/getallheaders.git",
"reference": "120b605dfeb996808c31b6477290a714d356e822"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
"reference": "120b605dfeb996808c31b6477290a714d356e822",
"shasum": ""
},
"require": {
"php": ">=5.6"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^5 || ^6.5"
},
"type": "library",
"autoload": {
"files": [
"src/getallheaders.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ralph Khattar",
"email": "ralph.khattar@gmail.com"
}
],
"description": "A polyfill for getallheaders.",
"support": {
"issues": "https://github.com/ralouphie/getallheaders/issues",
"source": "https://github.com/ralouphie/getallheaders/tree/develop"
},
"time": "2019-03-08T08:55:37+00:00"
},
{
"name": "rector/rector",
"version": "0.12.23",
@ -8175,6 +8840,269 @@
],
"time": "2022-09-09T09:34:27+00:00"
},
{
"name": "symfony/ux-cropperjs",
"version": "v2.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/ux-cropperjs.git",
"reference": "123b30ff6b539625bcf50413b945a16de6bc9d6e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/ux-cropperjs/zipball/123b30ff6b539625bcf50413b945a16de6bc9d6e",
"reference": "123b30ff6b539625bcf50413b945a16de6bc9d6e",
"shasum": ""
},
"require": {
"intervention/image": "^2.5",
"php": ">=7.2.5",
"symfony/config": "^4.4.17|^5.0|^6.0",
"symfony/dependency-injection": "^4.4.17|^5.0|^6.0",
"symfony/form": "^4.4.17|^5.0|^6.0",
"symfony/http-kernel": "^4.4.17|^5.0|^6.0",
"symfony/validator": "^4.4.17|^5.0|^6.0"
},
"conflict": {
"symfony/flex": "<1.13"
},
"require-dev": {
"symfony/framework-bundle": "^4.4.17|^5.0|^6.0",
"symfony/phpunit-bridge": "^5.2|^6.0",
"symfony/twig-bundle": "^4.4.17|^5.0|^6.0",
"symfony/var-dumper": "^4.4.17|^5.0|^6.0"
},
"type": "symfony-bundle",
"extra": {
"thanks": {
"name": "symfony/ux",
"url": "https://github.com/symfony/ux"
}
},
"autoload": {
"psr-4": {
"Symfony\\UX\\Cropperjs\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Titouan Galopin",
"email": "galopintitouan@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Cropper.js integration for Symfony",
"homepage": "https://symfony.com",
"keywords": [
"symfony-ux"
],
"support": {
"source": "https://github.com/symfony/ux-cropperjs/tree/v2.5.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": "2022-11-01T17:19:11+00:00"
},
{
"name": "symfony/ux-dropzone",
"version": "v2.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/ux-dropzone.git",
"reference": "897dbc3a8e61510c3f9c2ee7fee5596eb9cbf1d6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/ux-dropzone/zipball/897dbc3a8e61510c3f9c2ee7fee5596eb9cbf1d6",
"reference": "897dbc3a8e61510c3f9c2ee7fee5596eb9cbf1d6",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/config": "^4.4.17|^5.0|^6.0",
"symfony/dependency-injection": "^4.4.17|^5.0|^6.0",
"symfony/form": "^4.4.17|^5.0|^6.0",
"symfony/http-kernel": "^4.4.17|^5.0|^6.0"
},
"require-dev": {
"symfony/framework-bundle": "^4.4.17|^5.0|^6.0",
"symfony/phpunit-bridge": "^5.2|^6.0",
"symfony/twig-bundle": "^4.4.17|^5.0|^6.0",
"symfony/var-dumper": "^4.4.17|^5.0|^6.0"
},
"type": "symfony-bundle",
"extra": {
"thanks": {
"name": "symfony/ux",
"url": "https://github.com/symfony/ux"
}
},
"autoload": {
"psr-4": {
"Symfony\\UX\\Dropzone\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Titouan Galopin",
"email": "galopintitouan@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "File input dropzones for Symfony Forms",
"homepage": "https://symfony.com",
"keywords": [
"symfony-ux"
],
"support": {
"source": "https://github.com/symfony/ux-dropzone/tree/v2.5.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": "2022-09-08T14:28:44+00:00"
},
{
"name": "symfony/ux-turbo",
"version": "v2.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/ux-turbo.git",
"reference": "3476bf0030937824137ad0aa1c9797d7c6085882"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/ux-turbo/zipball/3476bf0030937824137ad0aa1c9797d7c6085882",
"reference": "3476bf0030937824137ad0aa1c9797d7c6085882",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/webpack-encore-bundle": "^1.11"
},
"conflict": {
"symfony/flex": "<1.13"
},
"require-dev": {
"doctrine/annotations": "^1.12",
"doctrine/doctrine-bundle": "^2.2",
"doctrine/orm": "^2.8 | 3.0",
"phpstan/phpstan": "^0.12",
"symfony/debug-bundle": "^5.2|^6.0",
"symfony/expression-language": "^5.4|^6.0",
"symfony/form": "^5.2|^6.0",
"symfony/framework-bundle": "^5.2|^6.0",
"symfony/mercure-bundle": "^0.3",
"symfony/messenger": "^5.2|^6.0",
"symfony/panther": "^1.0|^2.0",
"symfony/phpunit-bridge": "^5.2.1|^6.0",
"symfony/property-access": "^5.2|^6.0",
"symfony/security-core": "^5.2|^6.0",
"symfony/stopwatch": "^5.2|^6.0",
"symfony/twig-bundle": "^5.2|^6.0",
"symfony/web-profiler-bundle": "^5.2|^6.0"
},
"type": "symfony-bundle",
"extra": {
"thanks": {
"name": "symfony/ux",
"url": "https://github.com/symfony/ux"
}
},
"autoload": {
"psr-4": {
"Symfony\\UX\\Turbo\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Kévin Dunglas",
"email": "kevin@dunglas.fr"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Hotwire Turbo integration for Symfony",
"homepage": "https://symfony.com",
"keywords": [
"hotwire",
"javascript",
"mercure",
"symfony-ux",
"turbo",
"turbo-stream"
],
"support": {
"source": "https://github.com/symfony/ux-turbo/tree/v2.5.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": "2022-09-08T14:28:44+00:00"
},
{
"name": "symfony/validator",
"version": "v6.1.7",

View File

@ -19,4 +19,9 @@ return [
SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle::class => ['all' => true],
SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true],
Liip\ImagineBundle\LiipImagineBundle::class => ['all' => true],
Symfony\UX\Dropzone\DropzoneBundle::class => ['all' => true],
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
Flasher\Symfony\FlasherSymfonyBundle::class => ['all' => true],
Flasher\SweetAlert\Symfony\FlasherSweetAlertSymfonyBundle::class => ['all' => true],
Symfony\UX\Cropperjs\CropperjsBundle::class => ['all' => true],
];

View File

@ -1,13 +1,15 @@
# Documentation on how to configure the bundle can be found at: https://symfony.com/doc/current/bundles/LiipImagineBundle/basic-usage.html
liip_imagine:
# valid drivers options include "gd" or "gmagick" or "imagick"
driver: "gd"
driver: "imagick"
twig:
mode: lazy
filter_sets:
squared_thumbnail_small:
filters:
thumbnail:
size: [100, 100]
size: [120, 120]
mode: outbound
allow_upscale: true
squared_thumbnail_medium:

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20221115104149 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE user ADD tmp_avatar VARCHAR(255) DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE user DROP tmp_avatar');
}
}

View File

@ -1,11 +1,17 @@
{
"devDependencies": {
"@babel/preset-react": "^7.0.0",
"@hotwired/stimulus": "^3.0.0",
"@hotwired/turbo": "^7.0.1",
"@popperjs/core": "^2.11.6",
"@symfony/stimulus-bridge": "^3.0.0",
"@symfony/ux-cropperjs": "file:vendor/symfony/ux-cropperjs/Resources/assets",
"@symfony/ux-dropzone": "file:vendor/symfony/ux-dropzone/Resources/assets",
"@symfony/ux-turbo": "file:vendor/symfony/ux-turbo/Resources/assets",
"@symfony/webpack-encore": "^2.0.0",
"autoprefixer": "^10.4.7",
"core-js": "^3.0.0",
"cropperjs": "^1.5.9",
"file-loader": "^6.0.0",
"jquery": "^3.6.0",
"less-loader": "^10.0.0",
@ -13,6 +19,7 @@
"regenerator-runtime": "^0.13.2",
"sass": "^1.50.0",
"sass-loader": "^12.0.0",
"sweetalert2": "^11.6.8",
"ts-loader": "^9.0.0",
"typescript": "^4.6.4",
"webpack-manifest-plugin": "^5.0.0",
@ -44,6 +51,7 @@
"postcss-import": "^14.1.0",
"postcss-loader": "^7.0.0",
"quill": "^1.3.7",
"quill-html-edit-button": "^2.2.12"
"quill-html-edit-button": "^2.2.12",
"toastify-js": "^1.12.0"
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Command;
use App\Repository\UserRepository;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'cron:run',
description: 'Cleanup stale avatars',
)]
class CronRunCommand extends Command
{
public function __construct(private readonly UserRepository $userRepository)
{
parent::__construct();
}
protected function configure(): void
{
/*
$this
->addArgument('arg1', InputArgument::OPTIONAL, 'Argument description')
->addOption('option1', null, InputOption::VALUE_NONE, 'Option description')
;
*/
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$verbose = $input->getOption('verbose');
$staleCount = $this->deleteStaleAvatars();
if ($verbose) {
if ($staleCount > 0) {
$io->writeln("There were " . $staleCount . " stale avatars.");
} else {
$io->writeln("There were no stale avatars younger than 24 hours.");
}
}
return Command::SUCCESS;
}
private function deleteStaleAvatars(): int
{
$avatarDir = dirname(__DIR__, 2) . '/public/uploads/avatars';
$keepTime = 86400; // 24 hours
$deletedFiles = 0;
foreach (glob($avatarDir . '/*') as $entry) {
$filectime = filectime($entry);
if ($filectime && $filectime + $keepTime < time()) {
// check if it in use
if ($user = $this->userRepository->findOneBy(['avatar' => basename($entry)])) {
echo basename($entry) . " is in use by: " . $user->getUserIdentifier() . PHP_EOL;
}
unlink($entry);
$deletedFiles++;
}
}
return $deletedFiles;
}
}

View File

@ -92,8 +92,8 @@ class SecurityController extends AbstractController
return $this->render(view: '@default/security/registration_finished.html.twig');
}
return $this->render(view: '@default/security/register.html.twig', parameters: [
'registrationForm' => $form->createView(),
return $this->renderForm(view: '@default/security/register.html.twig', parameters: [
'registrationForm' => $form,
]);
}
@ -142,8 +142,8 @@ class SecurityController extends AbstractController
);
}
return $this->render(view: '@default/security/forgot_password.html.twig', parameters: [
'requestForm' => $form->createView(),
return $this->renderForm(view: '@default/security/forgot_password.html.twig', parameters: [
'requestForm' => $form,
]);
}
@ -220,8 +220,8 @@ class SecurityController extends AbstractController
return $this->redirectToRoute(route: 'app_main');
}
return $this->render(view: '@default/security/reset_password.html.twig', parameters: [
'resetForm' => $form->createView(),
return $this->renderForm(view: '@default/security/reset_password.html.twig', parameters: [
'resetForm' => $form,
]);
}
@ -330,7 +330,7 @@ class SecurityController extends AbstractController
#[Route('/security/resend/verify_email', name: 'security_resend_verify_email')]
public function resendVerifyEmail(Request $request, UserRepository $userRepository)
public function resendVerifyEmail(Request $request, UserRepository $userRepository): Response
{
if ($request->isMethod('POST')) {

View File

@ -6,6 +6,8 @@ use App\Entity\User;
use App\Form\EditProfileFormType;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Flasher\Prime\FlasherInterface;
use Flasher\SweetAlert\Prime\SweetAlertFactory;
use Sunrise\Slugger\Slugger;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\UploadedFile;
@ -24,50 +26,6 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
*/
class UserController extends BaseController
{
#[Route(path: '/profile/edit/{username}', name: 'app_profile_edit')]
public function editProfile(Request $request, UserRepository $userRepository, UserPasswordHasherInterface $userPasswordHasher, EntityManagerInterface $entityManager, string $username = ''): Response
{
if ($username !== '') {
if ($this->isGranted(attribute: 'ROLE_ADMIN')) {
$user = $userRepository->findOneBy([
'username' => $username,
]);
} else {
throw new AccessDeniedException(message: 'Only admins are allowed to edit foreign profiles.');
}
} else {
$user = $this->getUser();
}
$form = $this->createForm(type: EditProfileFormType::class, data: $user);
$form->handleRequest(request: $request);
if ($form->isSubmitted() && $form->isValid()) {
$user = $form->getData();
// if there's a new password, use it
if ($form->get(name: 'newPassword')->getData())
$user->setPassword(
password: $userPasswordHasher->hashPassword(
user: $user,
plainPassword: $form->get(name: 'newPassword')->getData()
)
);
$entityManager->persist(entity: $user);
$entityManager->flush();
return $this->redirectToRoute(route: 'app_main');
};
return $this->renderForm(view: '@default/user/edit_profile.html.twig', parameters: [
'user' => $user,
'userForm' => $form
]);
}
#[Route(path: '/profile/{username}', name: 'app_profile')]
public function showProfile(UserRepository $userRepository, string $username = ''): Response
{
@ -85,6 +43,61 @@ class UserController extends BaseController
]);
}
#[Route(path: '/profile/edit/{username}', name: 'app_profile_edit')]
public function editProfile(Request $request,
UserRepository $userRepository,
UserPasswordHasherInterface $userPasswordHasher,
EntityManagerInterface $entityManager,
string $username = ''): Response
{
$user = $this->getUser();
$editUser = $userRepository->findOneBy(['username' => $username]);
if ($username !== $editUser->getUsername()) {
if (!$this->isGranted(attribute: 'ROLE_ADMIN')) {
$this->addFlash(type: 'error', message: 'Only admins are allowed to edit foreign profiles.');
return $this->redirectToRoute(route: 'app_main');
}
}
$form = $this->createForm(type: EditProfileFormType::class, data: $user);
$form->handleRequest(request: $request);
if ($form->isSubmitted() && $form->isValid()) {
$user = $form->getData();
// if there's a new password, use it
if ($form->get(name: 'newPassword')->getData()) {
$user->setPassword(
password: $userPasswordHasher->hashPassword(
user: $user,
plainPassword: $form->get(name: 'newPassword')->getData()
)
);
}
if ($user->getTmpAvatar()) {
$user->setAvatar($user->getTmpAvatar());
$user->setTmpAvatar('');
}
$entityManager->persist(entity: $user);
$entityManager->flush();
$this->addFlash(type: 'success', message: 'Profile successfully updated.');
return $this->redirectToRoute(route: 'app_main');
};
return $this->renderForm(view: '@default/user/edit_profile.html.twig', parameters: [
'user' => $editUser,
'userForm' => $form
]);
}
#[Route(path: '/list_users/', name: 'app_list_user')]
public function listUsers(UserRepository $userRepository): Response
{
@ -96,7 +109,7 @@ class UserController extends BaseController
}
// TODO move to a helper class
function humanFilesize($bytes, $decimals = 2)
function humanFilesize($bytes, $decimals = 2): string
{
$sz = 'BKMGTP';
$factor = floor((strlen($bytes) - 1) / 3);
@ -132,7 +145,8 @@ class UserController extends BaseController
$cleanFilename = $slugger->slugify($originalFilename);
$newFilename = $cleanFilename . '-' . uniqid() . '.' . $uploadedAvatar->guessExtension();
$uploadedAvatar->move($destination, $newFilename);
$user->setAvatar($newFilename);
// Store the tmp name, use it on real form submit.
$user->setTmpAvatar($newFilename);
$entityManager->persist(entity: $user);
$entityManager->flush();

View File

@ -64,6 +64,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\Column]
private ?DateTimeImmutable $agreedTermsAt = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $tmpAvatar = null;
public function __construct()
{
$this->projects = new ArrayCollection();
@ -315,4 +318,16 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
{
// TODO: Implement eraseCredentials() method.
}
public function getTmpAvatar(): ?string
{
return $this->tmpAvatar;
}
public function setTmpAvatar(?string $tmpAvatar): self
{
$this->tmpAvatar = $tmpAvatar;
return $this;
}
}

View File

@ -5,34 +5,65 @@ namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Image;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\UX\Cropperjs\Form\CropperType;
use Symfony\UX\Dropzone\Form\DropzoneType;
class EditProfileFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add(child: 'username')
->add(child: 'firstName')
->add(child: 'lastName')
->add(child: 'email', type: EmailType::class)
->add(child: 'username', options: [
'attr' => [
'autocomplete' => 'nickname'
]
])
->add(child: 'firstName', options: [
'required' => false,
'attr' => [
'autocomplete' => 'given-name'
]
])
->add(child: 'lastName', options: [
'required' => false,
'attr' => [
'autocomplete' => 'family-name'
]
])
->add(child: 'email', type: EmailType::class, options: [
'attr' => [
'autocomplete' => 'email'
]
])
/*
->add(child: 'avatarName', type: DropzoneType::class, options: [
'mapped' => false,
'required' => false,
'attr' => [
'data-controller' => 'upload-avatar',
]
])
*/
->add(child: 'newPassword', type: RepeatedType::class, options: [
'type' => PasswordType::class,
'mapped' => false,
'invalid_message' => 'The password fields must match.',
'options' => ['attr' => ['class' => 'password-field', 'autocomplete' => 'off']],
'required' => false,
'first_options' => ['label' => 'Password'],
'second_options' => ['label' => 'Repeat Password (only needed if you want to update the password)'],
'constraints' => [new Length(exactly: ['min' => 6])]
])
;
'constraints' => [new Length(exactly: ['min' => 6])],
'options' => [
'attr' => [
'autocomplete' => 'new-password'
]
]
]);
}
public function configureOptions(OptionsResolver $resolver): void

View File

@ -72,6 +72,12 @@
"config/packages/nelmio_cors.yaml"
]
},
"php-flasher/flasher-sweetalert-symfony": {
"version": "v1.7.0"
},
"php-flasher/flasher-symfony": {
"version": "v1.7.0"
},
"sensio/framework-extra-bundle": {
"version": "6.2",
"recipe": {
@ -271,6 +277,15 @@
"templates/base.html.twig"
]
},
"symfony/ux-cropperjs": {
"version": "v2.5.0"
},
"symfony/ux-dropzone": {
"version": "v2.5.0"
},
"symfony/ux-turbo": {
"version": "v2.5.0"
},
"symfony/validator": {
"version": "6.1",
"recipe": {

View File

@ -1,4 +1,6 @@
<nav class="navbar navbar-expand-md navbar-dark bg-dark sticky-top px-sm-5 border-0">
<nav
class="navbar navbar-expand-md navbar-dark sticky-top px-sm-5 border-0 {{ is_granted('ROLE_PREVIOUS_ADMIN') ? 'bg-light' : 'bg-dark' }}"
>
<a class="navbar-brand border-0" href="{{ path('app_main') }}">
<img src="{{ asset('build/images/24unix/24_logo_bg_64x64.png') }}" alt="24unix.net" id="site-logo">
@ -23,7 +25,7 @@
<button type="button" id="navbar-dropdown" data-bs-target="#dropdown-menu" data-bs-toggle="dropdown"
class="btn btn-outline-dark dropdown-toggle button-login">
{% if app.user.avatar %}
<img class="rounded-circle"
<img class="rounded-circle border border-primary"
width="50px"
src="{{ avatar_asset(app.user.avatar)|imagine_filter('squared_thumbnail_small') }}"
alt="profile image"/>
@ -34,7 +36,7 @@
<div class="dropdown-menu dropdown-menu-dark dropdown-menu-end" id="dropdown-menu"
aria-labelledby="navbar-dropdown">
<a class="dropdown-item" href="{{ path('app_profile_edit') }}">
<a class="dropdown-item" href="{{ path('app_profile_edit', { 'username': app.user.username }) }}">
<span class="fa fa-lg fa-fw fa-user" aria-hidden="true"></span>
Profile</a>
<a class="dropdown-item" href="#">
@ -49,6 +51,15 @@
<div class="dropdown-divider"></div>
{% endif %}
{% if is_granted('ROLE_PREVIOUS_ADMIN') %}
<a class="dropdown-item" href="{{ path('app_main', {
'_switch_user': '_exit'
}) }}">
<span class="fa fa-lg fa-fw fa-backward" aria-hidden="true"></span>
Back to Admin</a>
<div class="dropdown-divider"></div>
{% endif %}
<a class="dropdown-item" href="{{ path('security_logout') }}">
<span class="fa fa-lg fa-fw fa-sign-out" aria-hidden="true"></span>&nbsp;
Logout

View File

@ -16,24 +16,25 @@
<!-- Matomo -->
<script>
let _paq = window._paq = window._paq || []
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["trackPageView"])
_paq.push(["enableLinkTracking"]);
(function () {
const u = "https://analytics.24unix.net/"
_paq.push(["setTrackerUrl", u + "matomo.php"])
_paq.push(["setSiteId", "1"])
const d = document, g = d.createElement("script"), s = d.getElementsByTagName("script")[0]
g.async = true
g.src = u + "matomo.js"
s.parentNode.insertBefore(g, s)
})()
let _paq = window._paq = window._paq || []
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["trackPageView"])
_paq.push(["enableLinkTracking"]);
(function () {
const u = "https://analytics.24unix.net/"
_paq.push(["setTrackerUrl", u + "matomo.php"])
_paq.push(["setSiteId", "1"])
const d = document, g = d.createElement("script"), s = d.getElementsByTagName("script")[0]
g.async = true
g.src = u + "matomo.js"
s.parentNode.insertBefore(g, s)
})()
</script>
<!-- End Matomo Code -->
{% endblock %}
</head>
<body>
<body data-turbo="false">
{% include '@default/_header.html.twig' %}
@ -74,11 +75,20 @@
<div class="col m-3" id="main_content">
{% for message in app.flashes('success') %}
<div class="alert alert-success">
{{ message }}
</div>
{% endfor %}
{#
<div data-turbo="false">
{% for message in app.flashes('success') %}
<div id="react"></div>
<div
{{ stimulus_controller('toastify', {
flash: message
}) }}
>
</div>
dump(app)
{% endfor %}
</div>
#}
{% for message in app.flashes('error') %}
<div class="alert alert-danger">
@ -101,5 +111,7 @@
{% include '@default/_footer.html.twig' %}
</body>
</html>

View File

@ -10,4 +10,5 @@
{% if page.modifiedAt %}
(last update: {{ page.modifiedAt | ago }})
{% endif %}
{% endblock %}
{% endblock %}

View File

@ -20,12 +20,26 @@
<div class="col-12">
<label for="inputUsername">Username or eMail</label>
<input type="text" name="username" id="inputUsername"
class="form-control" {# value="{{ last_username }}" #} required autofocus>
<input type="text"
name="username"
autocomplete="nickname"
id="inputUsername"
class="form-control"
value="{{ last_username }}"
required
autofocus
>
</div>
<div class="col-12">
<label for="inputPassword">Password</label>
<input type="password" name="password" id="inputPassword" class="form-control" required>
<input
type="password"
name="password"
autocomplete="current-password"
id="inputPassword"
class="form-control"
required
>
</div>
<div class="form-check mx-3">
<label>

View File

@ -7,25 +7,31 @@
{% block body %}
<div class="container box rounded bg-dark mt-5 mb-5">
{{ form_start(userForm) }}
<div class="row">
<div class="col-md-3 border-right">
<div class="d-flex flex-column align-items-center text-center p-3 py-5">
{#
{% if user.avatar is not null %}
<img class="rounded-circle mt-5 mb-4"
src="{{ avatar_asset(user.avatar)|imagine_filter('squared_thumbnail_small') }}"
alt="profile image"/>
{% endif %}
{# {{ form_row(userForm.avatarName, { 'label': false }) }} #}
<form
action="{{ path('user_upload_avatar', { id: user.id}) }}"
method="POST"
enctype="multipart/form-data"
class="dropzone" id="dropzoneForm">
</form>
<div id="preview-content">{{ avatar_asset(user.avatar)|imagine_filter('squared_thumbnail_small') }}</div>
#}
<div
class="dropzone"
id="avatarDropzone"
style="width:172px;"
{{ stimulus_controller('upload_avatar', {
avatarImage: avatar_asset(user.avatar)|imagine_filter('squared_thumbnail_small') ,
userId: user.id
}) }}
>
</div>
<span class="font-weight-bold">{{ user.username }}</span>
<span class="text-white-50"><span class="fa fa-lg fa-envelope me-1"></span>{{ user.email }}</span>
<span class="text-white-50"><span class="fa fa-lg fa-envelope me-1"></span><a href="mailto:{{ user.email }}">{{ user.email }}</a></span>
</div>
</div>
<div class="col-md-8 border-right">
@ -34,7 +40,6 @@
<h4 class="text-right">User Profile</h4>
</div>
<div class="row mt-2">
{{ form_start(userForm) }}
{{ form_row(userForm.username) }}
{{ form_row(userForm.firstName) }}
{{ form_row(userForm.lastName) }}
@ -42,7 +47,6 @@
{{ form_row(userForm.newPassword.first) }}
{{ form_row(userForm.newPassword.second) }}
{{ form_rest(userForm) }}
{{ form_end(userForm) }}
</div>
<div class="mb-5 text-center float-end">
<button class="btn btn-primary profile-button" type="submit">Save Profile</button>
@ -50,6 +54,9 @@
</div>
</div>
</div>
{{ form_end(userForm) }}
</div>
{% endblock %}

View File

@ -0,0 +1,46 @@
{% extends '@default/base.html.twig' %}
{% block title %}
Profile of {{ user.username }}
{% endblock %}
{% block body %}
<div class="container box rounded bg-dark mt-5 mb-5">
<div class="row">
<div class="col-md-3 border-right">
<div class="d-flex flex-column align-items-center text-center p-3 py-5">
{% if user.avatar is not null %}
<img class="rounded-circle mt-5 mb-4"
src="{{ avatar_asset(user.avatar)|imagine_filter('squared_thumbnail_small') }}"
alt="profile image"/>
{% endif %}
<span class="font-weight-bold">{{ user.username }}</span>
{% if is_granted('ROLE_ADMIN') %}
<span class="font-weight-bold">
<a href="{{ path('app_main', { '_switch_user': user.username }) }}">switch user</a>
</span>
{% endif %}
<span class="text-white-50"><i class="fa fa-lg fa-envelope me-1"></i><a href="mailto:{{ user.email }}">{{ user.email }}</a></span>
</div>
</div>
<div class="col-md-5 border-right">
<div class="p-3 py-5">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="text-right">User Profile</h4>
</div>
<div class="row mt-2">
<label class="labels" for="first-name">First Name</label>
<input type="text" disabled id="first-name" class="form-control" value="{{ user.firstName }}">
<label class="labels" for="last-name">Last Name</label>
<input type="text" disabled id="last-name" class="form-control" value="{{ user.lastName }}">
</div>
</div>
</div>
</div>
</div>
{% endblock %}

1
templates/themes/default Symbolic link
View File

@ -0,0 +1 @@
24unix.net

View File

@ -1,63 +0,0 @@
{% extends '@default/base.html.twig' %}
{% block title %}
Profile of {{ user.username }}
{% endblock %}
{% block body %}
<div class="container box rounded bg-dark mt-5 mb-5">
<div class="row">
<div class="col-md-3 border-right">
<div class="d-flex flex-column align-items-center text-center p-3 py-5">
{% if user.avatar is not null %}
<img class="rounded-circle mt-5 mb-4"
src="{{ avatar_asset(user.avatar)|imagine_filter('squared_thumbnail_small') }}" alt="profile image"/>
{% endif %}
<span class="font-weight-bold">{{ user.username }}</span>
{% if is_granted('ROLE_ADMIN') %}
<span class="font-weight-bold">
<a href="{{ path('app_main', { '_switch_user': app.user.username }) }}">switch user {{ user.username }}</a>
</span>
{% endif %}
<span class="text-white-50"><i class="fa fa-lg fa-envelope me-1"></i>{{ user.email }}</span>
</div>
</div>
<div class="col-md-5 border-right">
<div class="p-3 py-5">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="text-right">User Profile</h4>
</div>
<div class="row mt-2">
<div class="col-md-6">
<label class="labels" for="first-name">First Name</label>
<input type="text" disabled id="first-name" class="form-control" placeholder="First Name"
value="{{ user.firstName }}">
</div>
<div class="col-md-6">
<label class="labels" for="last-name">Last Name</label>
<input type="text" disabled id="last-name" class="form-control" value="{{ user.lastName }}"
placeholder="Last Name">
</div>
<div class="col-md-6">
<label class="labels" for="username">Username</label>
<input type="text" disabled class="form-control" id="username" placeholder="eMail address"
value="{{ user.email }}">
</div>
</div>
<!--
<div class="mt-5 text-center">
<button class="btn btn-primary profile-button" type="button">Save Profile</button>
</div>
-->
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -43,5 +43,9 @@ Encore
}
})
.autoProvidejQuery()
.enableStimulusBridge(
'./assets/controllers.json'
)
.enableReactPreset()
module.exports = Encore.getWebpackConfig()

View File

@ -461,6 +461,13 @@
dependencies:
"@babel/helper-plugin-utils" "^7.8.0"
"@babel/plugin-syntax-jsx@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0"
integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==
dependencies:
"@babel/helper-plugin-utils" "^7.18.6"
"@babel/plugin-syntax-logical-assignment-operators@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699"
@ -701,6 +708,39 @@
dependencies:
"@babel/helper-plugin-utils" "^7.18.6"
"@babel/plugin-transform-react-display-name@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz#8b1125f919ef36ebdfff061d664e266c666b9415"
integrity sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==
dependencies:
"@babel/helper-plugin-utils" "^7.18.6"
"@babel/plugin-transform-react-jsx-development@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz#dbe5c972811e49c7405b630e4d0d2e1380c0ddc5"
integrity sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==
dependencies:
"@babel/plugin-transform-react-jsx" "^7.18.6"
"@babel/plugin-transform-react-jsx@^7.18.6":
version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.19.0.tgz#b3cbb7c3a00b92ec8ae1027910e331ba5c500eb9"
integrity sha512-UVEvX3tXie3Szm3emi1+G63jyw1w5IcMY0FSKM+CRnKRI5Mr1YbCNgsSTwoTwKphQEG9P+QqmuRFneJPZuHNhg==
dependencies:
"@babel/helper-annotate-as-pure" "^7.18.6"
"@babel/helper-module-imports" "^7.18.6"
"@babel/helper-plugin-utils" "^7.19.0"
"@babel/plugin-syntax-jsx" "^7.18.6"
"@babel/types" "^7.19.0"
"@babel/plugin-transform-react-pure-annotations@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.18.6.tgz#561af267f19f3e5d59291f9950fd7b9663d0d844"
integrity sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==
dependencies:
"@babel/helper-annotate-as-pure" "^7.18.6"
"@babel/helper-plugin-utils" "^7.18.6"
"@babel/plugin-transform-regenerator@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz#585c66cb84d4b4bf72519a34cfce761b8676ca73"
@ -859,6 +899,18 @@
"@babel/types" "^7.4.4"
esutils "^2.0.2"
"@babel/preset-react@^7.0.0":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.18.6.tgz#979f76d6277048dc19094c217b507f3ad517dd2d"
integrity sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==
dependencies:
"@babel/helper-plugin-utils" "^7.18.6"
"@babel/helper-validator-option" "^7.18.6"
"@babel/plugin-transform-react-display-name" "^7.18.6"
"@babel/plugin-transform-react-jsx" "^7.18.6"
"@babel/plugin-transform-react-jsx-development" "^7.18.6"
"@babel/plugin-transform-react-pure-annotations" "^7.18.6"
"@babel/runtime@^7.8.4":
version "7.20.1"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.1.tgz#1148bb33ab252b165a06698fde7576092a78b4a9"
@ -935,6 +987,11 @@
resolved "https://registry.yarnpkg.com/@hotwired/stimulus/-/stimulus-3.1.1.tgz#652f08a8e1d5edcb407340e58818fcff463b5848"
integrity sha512-e0JpzIaYLsRRXevRDVs0yevabiCvieIWWCwh7VqVXjXM5AOHdjb7AjaKIj34zYFmY1N6HIRRfk915WVMYlHnDA==
"@hotwired/turbo@^7.0.1":
version "7.2.4"
resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-7.2.4.tgz#0d35541be32cfae3b4f78c6ab9138f5b21f28a21"
integrity sha512-c3xlOroHp/cCZHDOuLp6uzQYEbvXBUVaal0puXoGJ9M8L/KHwZ3hQozD4dVeSN9msHWLxxtmPT1TlCN7gFhj4w==
"@humanwhocodes/config-array@^0.11.6":
version "0.11.7"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.7.tgz#38aec044c6c828f6ed51d5d7ae3d9b9faf6dbb0f"
@ -1059,6 +1116,15 @@
loader-utils "^2.0.0"
schema-utils "^3.0.0"
"@symfony/ux-cropperjs@file:vendor/symfony/ux-cropperjs/Resources/assets":
version "1.1.0"
"@symfony/ux-dropzone@file:vendor/symfony/ux-dropzone/Resources/assets":
version "1.1.0"
"@symfony/ux-turbo@file:vendor/symfony/ux-turbo/Resources/assets":
version "0.1.0"
"@symfony/webpack-encore@^2.0.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@symfony/webpack-encore/-/webpack-encore-2.1.0.tgz#353a1b8bc38022046cbbc3d627c4076aca2e28c3"
@ -2042,6 +2108,11 @@ cosmiconfig@^7.0.0:
path-type "^4.0.0"
yaml "^1.10.0"
cropperjs@^1.5.9:
version "1.5.12"
resolved "https://registry.yarnpkg.com/cropperjs/-/cropperjs-1.5.12.tgz#d9c0db2bfb8c0d769d51739e8f916bbc44e10f50"
integrity sha512-re7UdjE5UnwdrovyhNzZ6gathI4Rs3KGCBSc8HCIjUo5hO42CtzyblmWLj6QWVw7huHyDMfpKxhiO2II77nhDw==
cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@ -5111,6 +5182,11 @@ svgo@^2.7.0:
picocolors "^1.0.0"
stable "^0.1.8"
sweetalert2@^11.6.8:
version "11.6.8"
resolved "https://registry.yarnpkg.com/sweetalert2/-/sweetalert2-11.6.8.tgz#5a515284367bc21b9f7830cbfbbfd09341576cca"
integrity sha512-0YHMaqF3DC67EI9uZzHpbU34rQV3acEFlnUCYmDSDNkeNOSFtSlF4DbWilfln+iUYv9s9aqbREXmKZRJqh5G5w==
sync-rpc@^1.3.6:
version "1.3.6"
resolved "https://registry.yarnpkg.com/sync-rpc/-/sync-rpc-1.3.6.tgz#b2e8b2550a12ccbc71df8644810529deb68665a7"
@ -5173,6 +5249,11 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
toastify-js@^1.12.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/toastify-js/-/toastify-js-1.12.0.tgz#cc1c4f5c7e7380e854e20bedceb51980ea29f64d"
integrity sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==
toidentifier@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"