- Basic HTML templates
- Created entities & new migration - Added packages symfony/stimulus-bundle & symfony/webpack-encore-bundle
This commit is contained in:
parent
e8df5bf019
commit
3ed53ac05e
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/.yarn/** linguist-vendored
|
||||||
|
/.yarn/releases/* binary
|
||||||
|
/.yarn/plugins/**/* binary
|
||||||
|
/.pnp.* binary linguist-generated
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -20,4 +20,10 @@
|
|||||||
###< symfony/phpunit-bridge ###
|
###< symfony/phpunit-bridge ###
|
||||||
|
|
||||||
# IDE folders
|
# IDE folders
|
||||||
.idea
|
.idea
|
||||||
|
###> symfony/webpack-encore-bundle ###
|
||||||
|
/node_modules/
|
||||||
|
/public/build/
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
###< symfony/webpack-encore-bundle ###
|
||||||
|
28
assets/app.js
Normal file
28
assets/app.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import './bootstrap.js';
|
||||||
|
/*
|
||||||
|
* Welcome to your app's main JavaScript file!
|
||||||
|
*
|
||||||
|
* We recommend including the built version of this JavaScript file
|
||||||
|
* (and its CSS file) in your base layout (base.html.twig).
|
||||||
|
*/
|
||||||
|
|
||||||
|
// any CSS you import will output into a single css file (app.css in this case)
|
||||||
|
import './styles/app.scss';
|
||||||
|
|
||||||
|
require('fontawesome-free/js/all.js');
|
||||||
|
|
||||||
|
import * as mdb from 'mdb-ui-kit'; // lib
|
||||||
|
import { Input } from 'mdb-ui-kit'; // module
|
||||||
|
|
||||||
|
const $ = require('jquery');
|
||||||
|
// this "modifies" the jquery module: adding behavior to it
|
||||||
|
// the bootstrap module doesn't export/return anything
|
||||||
|
require('bootstrap');
|
||||||
|
|
||||||
|
// or you can include specific pieces
|
||||||
|
// require('bootstrap/js/dist/tooltip');
|
||||||
|
// require('bootstrap/js/dist/popover');
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('[data-toggle="popover"]').popover();
|
||||||
|
});
|
12
assets/bootstrap.js
vendored
Normal file
12
assets/bootstrap.js
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// assets/bootstrap.js
|
||||||
|
import { startStimulusApp } from '@symfony/stimulus-bridge';
|
||||||
|
|
||||||
|
// Registers Stimulus controllers from controllers.json and in the controllers/ directory
|
||||||
|
export const app = startStimulusApp(require.context(
|
||||||
|
'@symfony/stimulus-bridge/lazy-controller-loader!./controllers',
|
||||||
|
true,
|
||||||
|
/\.(j|t)sx?$/
|
||||||
|
));
|
||||||
|
|
||||||
|
// register any custom, 3rd party controllers here
|
||||||
|
// app.register('some_controller_name', SomeImportedController);
|
4
assets/controllers.json
Normal file
4
assets/controllers.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"controllers": [],
|
||||||
|
"entrypoints": []
|
||||||
|
}
|
16
assets/controllers/hello_controller.js
Normal file
16
assets/controllers/hello_controller.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
67
assets/styles/app.scss
Normal file
67
assets/styles/app.scss
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
body {
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
// customize some Bootstrap variables
|
||||||
|
$primary: darken(#428bca, 20%);
|
||||||
|
|
||||||
|
// the ~ allows you to reference things in node_modules
|
||||||
|
@import "~bootstrap/scss/bootstrap";
|
||||||
|
@import '~mdb-ui-kit/css/mdb.min.css';
|
||||||
|
|
||||||
|
// importing core styling file
|
||||||
|
@import "~fontawesome-free/scss/fontawesome.scss";
|
||||||
|
|
||||||
|
// our project needs Classic Solid, Brands, and Sharp Solid
|
||||||
|
@import "~fontawesome-free/scss/solid.scss";
|
||||||
|
@import "~fontawesome-free/scss/brands.scss";
|
||||||
|
@import "~fontawesome-free/scss/regular.scss";
|
||||||
|
|
||||||
|
/* for login page */
|
||||||
|
.gradient-custom {
|
||||||
|
/* fallback for old browsers */
|
||||||
|
background: #6a11cb;
|
||||||
|
|
||||||
|
/* Chrome 10-25, Safari 5.1-6 */
|
||||||
|
background: -webkit-linear-gradient(to right, rgba(106, 17, 203, 1), rgba(37, 117, 252, 1));
|
||||||
|
|
||||||
|
/* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
|
||||||
|
background: linear-gradient(to right, rgba(106, 17, 203, 1), rgba(37, 117, 252, 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 991.98px) {
|
||||||
|
main {
|
||||||
|
padding-left: 240px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
padding: 58px 0 0; /* Height of navbar */
|
||||||
|
box-shadow: 0 2px 5px 0 rgb(0 0 0 / 5%), 0 2px 10px 0 rgb(0 0 0 / 5%);
|
||||||
|
width: 240px;
|
||||||
|
z-index: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sidebar .active {
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 2px 5px 0 rgb(0 0 0 / 16%), 0 2px 10px 0 rgb(0 0 0 / 12%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-sticky {
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
height: calc(100vh - 48px);
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
|
||||||
|
}
|
@ -7,6 +7,7 @@
|
|||||||
"php": ">=8.1",
|
"php": ">=8.1",
|
||||||
"ext-ctype": "*",
|
"ext-ctype": "*",
|
||||||
"ext-iconv": "*",
|
"ext-iconv": "*",
|
||||||
|
"ext-intl": "*",
|
||||||
"doctrine/doctrine-bundle": "^2.10",
|
"doctrine/doctrine-bundle": "^2.10",
|
||||||
"doctrine/doctrine-migrations-bundle": "^3.2",
|
"doctrine/doctrine-migrations-bundle": "^3.2",
|
||||||
"doctrine/orm": "^2.16",
|
"doctrine/orm": "^2.16",
|
||||||
@ -35,11 +36,13 @@
|
|||||||
"symfony/runtime": "6.3.*",
|
"symfony/runtime": "6.3.*",
|
||||||
"symfony/security-bundle": "6.3.*",
|
"symfony/security-bundle": "6.3.*",
|
||||||
"symfony/serializer": "6.3.*",
|
"symfony/serializer": "6.3.*",
|
||||||
|
"symfony/stimulus-bundle": "^2.11",
|
||||||
"symfony/string": "6.3.*",
|
"symfony/string": "6.3.*",
|
||||||
"symfony/translation": "6.3.*",
|
"symfony/translation": "6.3.*",
|
||||||
"symfony/twig-bundle": "6.3.*",
|
"symfony/twig-bundle": "6.3.*",
|
||||||
"symfony/validator": "6.3.*",
|
"symfony/validator": "6.3.*",
|
||||||
"symfony/web-link": "6.3.*",
|
"symfony/web-link": "6.3.*",
|
||||||
|
"symfony/webpack-encore-bundle": "^2.0",
|
||||||
"symfony/yaml": "6.3.*",
|
"symfony/yaml": "6.3.*",
|
||||||
"twig/extra-bundle": "^2.12|^3.0",
|
"twig/extra-bundle": "^2.12|^3.0",
|
||||||
"twig/twig": "^2.12|^3.0"
|
"twig/twig": "^2.12|^3.0"
|
||||||
|
144
composer.lock
generated
144
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "0e74667a94096c60a3ca270a79c1331d",
|
"content-hash": "f0b903562ace7dd636451d35de6a7255",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "behat/transliterator",
|
"name": "behat/transliterator",
|
||||||
@ -6499,6 +6499,74 @@
|
|||||||
],
|
],
|
||||||
"time": "2023-05-23T14:45:45+00:00"
|
"time": "2023-05-23T14:45:45+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/stimulus-bundle",
|
||||||
|
"version": "v2.11.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/stimulus-bundle.git",
|
||||||
|
"reference": "e0e19de8df4d5b2bed57328ae69ef7904df660c7"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/e0e19de8df4d5b2bed57328ae69ef7904df660c7",
|
||||||
|
"reference": "e0e19de8df4d5b2bed57328ae69ef7904df660c7",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.1",
|
||||||
|
"symfony/config": "^5.4|^6.0",
|
||||||
|
"symfony/dependency-injection": "^5.4|^6.0",
|
||||||
|
"symfony/finder": "^5.4|^6.0",
|
||||||
|
"symfony/http-kernel": "^5.4|^6.0",
|
||||||
|
"twig/twig": "^2.15.3|^3.4.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/asset-mapper": "^6.3",
|
||||||
|
"symfony/framework-bundle": "^5.4|^6.0",
|
||||||
|
"symfony/phpunit-bridge": "^5.4|^6.0",
|
||||||
|
"symfony/twig-bundle": "^5.4|^6.0",
|
||||||
|
"zenstruck/browser": "^1.4"
|
||||||
|
},
|
||||||
|
"type": "symfony-bundle",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\UX\\StimulusBundle\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Integration with your Symfony app & Stimulus!",
|
||||||
|
"keywords": [
|
||||||
|
"symfony-ux"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/stimulus-bundle/tree/v2.11.1"
|
||||||
|
},
|
||||||
|
"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-08-28T18:00:50+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/stopwatch",
|
"name": "symfony/stopwatch",
|
||||||
"version": "v6.3.0",
|
"version": "v6.3.0",
|
||||||
@ -7350,6 +7418,77 @@
|
|||||||
],
|
],
|
||||||
"time": "2023-04-21T14:41:17+00:00"
|
"time": "2023-04-21T14:41:17+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/webpack-encore-bundle",
|
||||||
|
"version": "v2.0.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/webpack-encore-bundle.git",
|
||||||
|
"reference": "150fe022740fef908f4ca3d5950ce85ab040ec76"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/webpack-encore-bundle/zipball/150fe022740fef908f4ca3d5950ce85ab040ec76",
|
||||||
|
"reference": "150fe022740fef908f4ca3d5950ce85ab040ec76",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.1.0",
|
||||||
|
"symfony/asset": "^5.4 || ^6.2",
|
||||||
|
"symfony/config": "^5.4 || ^6.2",
|
||||||
|
"symfony/dependency-injection": "^5.4 || ^6.2",
|
||||||
|
"symfony/http-kernel": "^5.4 || ^6.2",
|
||||||
|
"symfony/service-contracts": "^1.1.9 || ^2.1.3 || ^3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/framework-bundle": "^5.4 || ^6.2",
|
||||||
|
"symfony/phpunit-bridge": "^5.4 || ^6.2",
|
||||||
|
"symfony/twig-bundle": "^5.4 || ^6.2",
|
||||||
|
"symfony/web-link": "^5.4 || ^6.2"
|
||||||
|
},
|
||||||
|
"type": "symfony-bundle",
|
||||||
|
"extra": {
|
||||||
|
"thanks": {
|
||||||
|
"name": "symfony/webpack-encore",
|
||||||
|
"url": "https://github.com/symfony/webpack-encore"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\WebpackEncoreBundle\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Integration with your Symfony app & Webpack Encore!",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/symfony/webpack-encore-bundle/issues",
|
||||||
|
"source": "https://github.com/symfony/webpack-encore-bundle/tree/v2.0.1"
|
||||||
|
},
|
||||||
|
"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-05-31T14:28:33+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/yaml",
|
"name": "symfony/yaml",
|
||||||
"version": "v6.3.3",
|
"version": "v6.3.3",
|
||||||
@ -9895,7 +10034,8 @@
|
|||||||
"platform": {
|
"platform": {
|
||||||
"php": ">=8.1",
|
"php": ">=8.1",
|
||||||
"ext-ctype": "*",
|
"ext-ctype": "*",
|
||||||
"ext-iconv": "*"
|
"ext-iconv": "*",
|
||||||
|
"ext-intl": "*"
|
||||||
},
|
},
|
||||||
"platform-dev": [],
|
"platform-dev": [],
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.6.0"
|
||||||
|
@ -11,4 +11,6 @@ return [
|
|||||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||||
|
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
||||||
|
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
twig:
|
twig:
|
||||||
|
form_themes: ['bootstrap_5_layout.html.twig']
|
||||||
default_path: '%kernel.project_dir%/templates'
|
default_path: '%kernel.project_dir%/templates'
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
|
45
config/packages/webpack_encore.yaml
Normal file
45
config/packages/webpack_encore.yaml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
webpack_encore:
|
||||||
|
# The path where Encore is building the assets - i.e. Encore.setOutputPath()
|
||||||
|
output_path: '%kernel.project_dir%/public/build'
|
||||||
|
# If multiple builds are defined (as shown below), you can disable the default build:
|
||||||
|
# output_path: false
|
||||||
|
|
||||||
|
# Set attributes that will be rendered on all script and link tags
|
||||||
|
script_attributes:
|
||||||
|
defer: true
|
||||||
|
# Uncomment (also under link_attributes) if using Turbo Drive
|
||||||
|
# https://turbo.hotwired.dev/handbook/drive#reloading-when-assets-change
|
||||||
|
# 'data-turbo-track': reload
|
||||||
|
# link_attributes:
|
||||||
|
# Uncomment if using Turbo Drive
|
||||||
|
# 'data-turbo-track': reload
|
||||||
|
|
||||||
|
# If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
|
||||||
|
# crossorigin: 'anonymous'
|
||||||
|
|
||||||
|
# Preload all rendered script and link tags automatically via the HTTP/2 Link header
|
||||||
|
# preload: true
|
||||||
|
|
||||||
|
# Throw an exception if the entrypoints.json file is missing or an entry is missing from the data
|
||||||
|
# strict_mode: false
|
||||||
|
|
||||||
|
# If you have multiple builds:
|
||||||
|
# builds:
|
||||||
|
# frontend: '%kernel.project_dir%/public/frontend/build'
|
||||||
|
|
||||||
|
# pass the build name as the 3rd argument to the Twig functions
|
||||||
|
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
|
||||||
|
|
||||||
|
framework:
|
||||||
|
assets:
|
||||||
|
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
|
||||||
|
|
||||||
|
#when@prod:
|
||||||
|
# webpack_encore:
|
||||||
|
# # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
|
||||||
|
# # Available in version 1.2
|
||||||
|
# cache: true
|
||||||
|
|
||||||
|
#when@test:
|
||||||
|
# webpack_encore:
|
||||||
|
# strict_mode: false
|
@ -20,5 +20,10 @@ services:
|
|||||||
- '../src/Entity/'
|
- '../src/Entity/'
|
||||||
- '../src/Kernel.php'
|
- '../src/Kernel.php'
|
||||||
|
|
||||||
# add more service definitions when explicit configuration is needed
|
|
||||||
# please note that last definitions always *replace* previous ones
|
# please note that last definitions always *replace* previous ones
|
||||||
|
gedmo.listener.timestampable:
|
||||||
|
class: Gedmo\Timestampable\TimestampableListener
|
||||||
|
tags:
|
||||||
|
- { name: doctrine.event_subscriber, connection: default }
|
||||||
|
calls:
|
||||||
|
- [ setAnnotationReader, [ '@annotation_reader' ] ]
|
31
migrations/Version20230909100148.php
Normal file
31
migrations/Version20230909100148.php
Normal 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 Version20230909100148 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 display_name VARCHAR(64) NOT NULL, ADD created_at DATETIME NOT NULL, ADD updated_at DATETIME 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 display_name, DROP created_at, DROP updated_at');
|
||||||
|
}
|
||||||
|
}
|
49
migrations/Version20231014080536.php
Normal file
49
migrations/Version20231014080536.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?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 Version20231014080536 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('CREATE TABLE comment (id INT AUTO_INCREMENT NOT NULL, page_id INT NOT NULL, user_id INT DEFAULT NULL, parent_comment_id INT DEFAULT NULL, deleted_by_user_id INT DEFAULT NULL, markdown LONGTEXT NOT NULL, html LONGTEXT NOT NULL, score INT NOT NULL, state VARCHAR(32) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME DEFAULT NULL, deleted_at DATETIME DEFAULT NULL, INDEX IDX_9474526CC4663E4 (page_id), INDEX IDX_9474526CA76ED395 (user_id), INDEX IDX_9474526CBF2AF943 (parent_comment_id), INDEX IDX_9474526CFCF2A97A (deleted_by_user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
$this->addSql('CREATE TABLE domain_page (id INT AUTO_INCREMENT NOT NULL, domain_id INT NOT NULL, path LONGTEXT NOT NULL, locked TINYINT(1) NOT NULL, comment_count INT NOT NULL, title LONGTEXT NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME DEFAULT NULL, deleted_at DATETIME DEFAULT NULL, INDEX IDX_B9D69B47115F0EE5 (domain_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
$this->addSql('CREATE TABLE page_view (id INT AUTO_INCREMENT NOT NULL, page_id INT NOT NULL, user_id INT DEFAULT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_7939B754C4663E4 (page_id), INDEX IDX_7939B754A76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
$this->addSql('ALTER TABLE comment ADD CONSTRAINT FK_9474526CC4663E4 FOREIGN KEY (page_id) REFERENCES domain_page (id)');
|
||||||
|
$this->addSql('ALTER TABLE comment ADD CONSTRAINT FK_9474526CA76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id)');
|
||||||
|
$this->addSql('ALTER TABLE comment ADD CONSTRAINT FK_9474526CBF2AF943 FOREIGN KEY (parent_comment_id) REFERENCES comment (id)');
|
||||||
|
$this->addSql('ALTER TABLE comment ADD CONSTRAINT FK_9474526CFCF2A97A FOREIGN KEY (deleted_by_user_id) REFERENCES `user` (id)');
|
||||||
|
$this->addSql('ALTER TABLE domain_page ADD CONSTRAINT FK_B9D69B47115F0EE5 FOREIGN KEY (domain_id) REFERENCES domain (id)');
|
||||||
|
$this->addSql('ALTER TABLE page_view ADD CONSTRAINT FK_7939B754C4663E4 FOREIGN KEY (page_id) REFERENCES domain_page (id)');
|
||||||
|
$this->addSql('ALTER TABLE page_view ADD CONSTRAINT FK_7939B754A76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE comment DROP FOREIGN KEY FK_9474526CC4663E4');
|
||||||
|
$this->addSql('ALTER TABLE comment DROP FOREIGN KEY FK_9474526CA76ED395');
|
||||||
|
$this->addSql('ALTER TABLE comment DROP FOREIGN KEY FK_9474526CBF2AF943');
|
||||||
|
$this->addSql('ALTER TABLE comment DROP FOREIGN KEY FK_9474526CFCF2A97A');
|
||||||
|
$this->addSql('ALTER TABLE domain_page DROP FOREIGN KEY FK_B9D69B47115F0EE5');
|
||||||
|
$this->addSql('ALTER TABLE page_view DROP FOREIGN KEY FK_7939B754C4663E4');
|
||||||
|
$this->addSql('ALTER TABLE page_view DROP FOREIGN KEY FK_7939B754A76ED395');
|
||||||
|
$this->addSql('DROP TABLE comment');
|
||||||
|
$this->addSql('DROP TABLE domain_page');
|
||||||
|
$this->addSql('DROP TABLE page_view');
|
||||||
|
}
|
||||||
|
}
|
31
package.json
Normal file
31
package.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "MyComments",
|
||||||
|
"packageManager": "yarn@3.6.3",
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.22.17",
|
||||||
|
"@babel/preset-env": "^7.22.15",
|
||||||
|
"@hotwired/stimulus": "^3.0.0",
|
||||||
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"@symfony/webpack-encore": "^4.4.0",
|
||||||
|
"babel-loader": "^9.1.3",
|
||||||
|
"bootstrap": "^5.3.1",
|
||||||
|
"core-js": "3.32.2",
|
||||||
|
"jquery": "^3.7.1",
|
||||||
|
"sass": "^1.66.1",
|
||||||
|
"sass-loader": "^13.0.0",
|
||||||
|
"webpack": "^5.88.2",
|
||||||
|
"webpack-cli": "^5.1.4",
|
||||||
|
"webpack-notifier": "^1.15.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@symfony/stimulus-bridge": "^3.2.2",
|
||||||
|
"fontawesome-free": "^1.0.4",
|
||||||
|
"mdb-ui-kit": "^6.4.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev-server": "encore dev-server",
|
||||||
|
"dev": "encore dev",
|
||||||
|
"watch": "encore dev --watch",
|
||||||
|
"build": "encore production --progress"
|
||||||
|
}
|
||||||
|
}
|
262
src/Command/AddUserCommand.php
Normal file
262
src/Command/AddUserCommand.php
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Repository\UserRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Exception;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||||
|
use Symfony\Component\Console\Exception\RuntimeException;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||||
|
use Symfony\Component\Stopwatch\Stopwatch;
|
||||||
|
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||||
|
use function Symfony\Component\String\u;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A console command that creates users and stores them in the database.
|
||||||
|
*
|
||||||
|
* To use this command, open a terminal window, enter into your project
|
||||||
|
* directory and execute the following:
|
||||||
|
*
|
||||||
|
* $ php bin/console app:add-user
|
||||||
|
*
|
||||||
|
* To output detailed information, increase the command verbosity:
|
||||||
|
*
|
||||||
|
* $ php bin/console app:add-user -vv
|
||||||
|
*
|
||||||
|
* See https://symfony.com/doc/current/console.html
|
||||||
|
*
|
||||||
|
* We use the default services.yaml configuration, so command classes are registered as services.
|
||||||
|
* See https://symfony.com/doc/current/console/commands_as_services.html
|
||||||
|
*
|
||||||
|
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||||
|
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||||
|
*/
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'app:add-user',
|
||||||
|
description: 'Creates users and stores them in the database'
|
||||||
|
)]
|
||||||
|
final class AddUserCommand extends Command
|
||||||
|
{
|
||||||
|
private SymfonyStyle $io;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly UserPasswordHasherInterface $passwordHasher,
|
||||||
|
private readonly ValidatorInterface $validator,
|
||||||
|
private readonly UserRepository $users
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setHelp($this->getCommandHelp())
|
||||||
|
// commands can optionally define arguments and/or options (mandatory and optional)
|
||||||
|
// see https://symfony.com/doc/current/components/console/console_arguments.html
|
||||||
|
->addArgument('display-name', InputArgument::OPTIONAL, 'The display name of the new user')
|
||||||
|
->addArgument('password', InputArgument::OPTIONAL, 'The plain password of the new user')
|
||||||
|
->addArgument('email', InputArgument::OPTIONAL, 'The email of the new user')
|
||||||
|
->addOption('admin', null, InputOption::VALUE_NONE, 'If set, the user is created as an administrator')
|
||||||
|
->addOption('owner', null, InputOption::VALUE_NONE, 'If set, the user is created as an owner')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This optional method is the first one executed for a command after configure()
|
||||||
|
* and is useful to initialize properties based on the input arguments and options.
|
||||||
|
*/
|
||||||
|
protected function initialize(InputInterface $input, OutputInterface $output): void
|
||||||
|
{
|
||||||
|
// SymfonyStyle is an optional feature that Symfony provides so you can
|
||||||
|
// apply a consistent look to the commands of your application.
|
||||||
|
// See https://symfony.com/doc/current/console/style.html
|
||||||
|
$this->io = new SymfonyStyle($input, $output);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is executed after initialize() and before execute(). Its purpose
|
||||||
|
* is to check if some of the options/arguments are missing and interactively
|
||||||
|
* ask the user for those values.
|
||||||
|
*
|
||||||
|
* This method is completely optional. If you are developing an internal console
|
||||||
|
* command, you probably should not implement this method because it requires
|
||||||
|
* quite a lot of work. However, if the command is meant to be used by external
|
||||||
|
* users, this method is a nice way to fall back and prevent errors.
|
||||||
|
*/
|
||||||
|
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||||
|
{
|
||||||
|
if (null !== $input->getArgument('password') && null !== $input->getArgument('email') && null !== $input->getArgument('display-name')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->io->title('Add User Command Interactive Wizard');
|
||||||
|
$this->io->text([
|
||||||
|
'If you prefer to not use this interactive wizard, provide the',
|
||||||
|
'arguments required by this command as follows:',
|
||||||
|
'',
|
||||||
|
' $ php bin/console app:add-user display-name password email@example.com',
|
||||||
|
'',
|
||||||
|
'Now we\'ll ask you for the value of all the missing command arguments.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Ask for the display name if it's not defined
|
||||||
|
$displayName = $input->getArgument('display-name');
|
||||||
|
if (null !== $displayName) {
|
||||||
|
$this->io->text(' > <info>Display Name</info>: '.$displayName);
|
||||||
|
} else {
|
||||||
|
$displayName = $this->io->ask('display Name', null, function($answer){
|
||||||
|
$validation = $this->validator->validatePropertyValue(User::class, 'displayName', $answer);
|
||||||
|
if($validation->count()) {
|
||||||
|
foreach($validation as $validationError) {
|
||||||
|
$this->io->error("{$validationError->getMessage()}");
|
||||||
|
}
|
||||||
|
throw new Exception("Invalid display name");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$input->setArgument('display-name', $displayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask for the password if it's not defined
|
||||||
|
/** @var string|null $password */
|
||||||
|
$password = $input->getArgument('password');
|
||||||
|
|
||||||
|
if (null !== $password) {
|
||||||
|
$this->io->text(' > <info>Password</info>: '.u('*')->repeat(u($password)->length()));
|
||||||
|
} else {
|
||||||
|
$password = $this->io->askHidden('Password (your type will be hidden)', function($answer){
|
||||||
|
// from the CLI we don't really want to impose too many restrictions on the password
|
||||||
|
if (empty($plainPassword)) {
|
||||||
|
throw new InvalidArgumentException('The password can not be empty.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (u($plainPassword)->trim()->length() < 6) {
|
||||||
|
throw new InvalidArgumentException('The password must be at least 6 characters long.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $answer;
|
||||||
|
});
|
||||||
|
$input->setArgument('password', $password);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask for the email if it's not defined
|
||||||
|
$email = $input->getArgument('email');
|
||||||
|
if (null !== $email) {
|
||||||
|
$this->io->text(' > <info>Email</info>: '.$email);
|
||||||
|
} else {
|
||||||
|
$email = $this->io->ask('Email', null, $this->validator->validateEmail(...));
|
||||||
|
$input->setArgument('email', $email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is executed after interact() and initialize(). It usually
|
||||||
|
* contains the logic to execute to complete this command task.
|
||||||
|
*/
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$stopwatch = new Stopwatch();
|
||||||
|
$stopwatch->start('add-user-command');
|
||||||
|
|
||||||
|
/** @var string $plainPassword */
|
||||||
|
$plainPassword = $input->getArgument('password');
|
||||||
|
|
||||||
|
/** @var string $email */
|
||||||
|
$email = $input->getArgument('email');
|
||||||
|
|
||||||
|
/** @var string $displayName */
|
||||||
|
$displayName = $input->getArgument('display-name');
|
||||||
|
|
||||||
|
$isAdmin = $input->getOption('admin');
|
||||||
|
$isOwner = $input->getOption('owner');
|
||||||
|
|
||||||
|
// make sure to validate the user data is correct
|
||||||
|
$this->validateUserData($plainPassword, $email, $displayName);
|
||||||
|
|
||||||
|
// create the user and hash its password
|
||||||
|
$user = new User();
|
||||||
|
$user->setDisplayName($displayName)
|
||||||
|
->setEmail($email)
|
||||||
|
->setRoles([$isAdmin ? User::ROLE_ADMIN : User::ROLE_USER])
|
||||||
|
->addRole(User::ROLE_USER);
|
||||||
|
|
||||||
|
if($isAdmin) {
|
||||||
|
$user->addRole(User::ROLE_ADMIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($isOwner) {
|
||||||
|
$user->addRole(User::ROLE_OWNER);
|
||||||
|
}
|
||||||
|
|
||||||
|
// See https://symfony.com/doc/5.4/security.html#registering-the-user-hashing-passwords
|
||||||
|
$hashedPassword = $this->passwordHasher->hashPassword($user, $plainPassword);
|
||||||
|
$user->setPassword($hashedPassword);
|
||||||
|
|
||||||
|
$this->entityManager->persist($user);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->io->success(sprintf('%s was successfully created: %s (%s)', $isAdmin ? 'Administrator user' : 'User', $user->getDisplayName(), $user->getEmail()));
|
||||||
|
|
||||||
|
$event = $stopwatch->stop('add-user-command');
|
||||||
|
if ($output->isVerbose()) {
|
||||||
|
$this->io->comment(sprintf('New user database id: %d / Elapsed time: %.2f ms / Consumed memory: %.2f MB', $user->getId(), $event->getDuration(), $event->getMemory() / (1024 ** 2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateUserData(string $plainPassword, string $email, string $displayName): void
|
||||||
|
{
|
||||||
|
// validate password and email if is not this input means interactive.
|
||||||
|
$this->validator->validatePassword($plainPassword);
|
||||||
|
$this->validator->validateEmail($email);
|
||||||
|
$this->validator->validateDisplayName($displayName);
|
||||||
|
|
||||||
|
// check if a user with the same email already exists.
|
||||||
|
$existingEmail = $this->users->findOneBy(['email' => $email]);
|
||||||
|
|
||||||
|
if (null !== $existingEmail) {
|
||||||
|
throw new RuntimeException(sprintf('There is already a user registered with the "%s" email.', $email));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The command help is usually included in the configure() method, but when
|
||||||
|
* it's too long, it's better to define a separate method to maintain the
|
||||||
|
* code readability.
|
||||||
|
*/
|
||||||
|
private function getCommandHelp(): string
|
||||||
|
{
|
||||||
|
return <<<'HELP'
|
||||||
|
The <info>%command.name%</info> command creates new users and saves them in the database:
|
||||||
|
|
||||||
|
<info>php %command.full_name%</info> <comment>display-name password email</comment>
|
||||||
|
|
||||||
|
By default the command creates regular users. To create administrator users,
|
||||||
|
add the <comment>--admin</comment> option:
|
||||||
|
|
||||||
|
<info>php %command.full_name%</info> display-name password email <comment>--admin</comment>
|
||||||
|
|
||||||
|
If you omit any of the three required arguments, the command will ask you to
|
||||||
|
provide the missing values:
|
||||||
|
|
||||||
|
# command will ask you for the email
|
||||||
|
<info>php %command.full_name%</info> <comment>display-name password</comment>
|
||||||
|
|
||||||
|
# command will ask you for the email and password
|
||||||
|
<info>php %command.full_name%</info> <comment>display-name</comment>
|
||||||
|
|
||||||
|
# command will ask you for all arguments
|
||||||
|
<info>php %command.full_name%</info>
|
||||||
|
HELP;
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,11 @@ class SecurityController extends AbstractController
|
|||||||
#[Route('/login', name: 'app_login')]
|
#[Route('/login', name: 'app_login')]
|
||||||
public function login(AuthenticationUtils $authenticationUtils): Response
|
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||||
{
|
{
|
||||||
|
// redirect already logged users to the dashboard
|
||||||
|
if($this->getUser()) {
|
||||||
|
return $this->redirectToRoute('app_dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
// get the login error if there is one
|
// get the login error if there is one
|
||||||
$error = $authenticationUtils->getLastAuthenticationError();
|
$error = $authenticationUtils->getLastAuthenticationError();
|
||||||
|
|
||||||
@ -29,6 +34,9 @@ class SecurityController extends AbstractController
|
|||||||
#[Route('/logout', name: 'app_logout', methods: ['GET'])]
|
#[Route('/logout', name: 'app_logout', methods: ['GET'])]
|
||||||
public function logout(Security $security): Response
|
public function logout(Security $security): Response
|
||||||
{
|
{
|
||||||
return $security->logout(false);
|
if($this->getUser()) {
|
||||||
|
$security->logout(false);
|
||||||
|
}
|
||||||
|
return $this->redirectToRoute('app_login');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
229
src/Entity/Comment.php
Normal file
229
src/Entity/Comment.php
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\CommentRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: CommentRepository::class)]
|
||||||
|
class Comment
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(inversedBy: 'comments')]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
private ?DomainPage $page = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(inversedBy: 'comments')]
|
||||||
|
private ?User $user = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT)]
|
||||||
|
private ?string $markdown = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT)]
|
||||||
|
private ?string $html = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'childComments')]
|
||||||
|
private ?self $parentComment = null;
|
||||||
|
|
||||||
|
#[ORM\OneToMany(mappedBy: 'parentComment', targetEntity: self::class)]
|
||||||
|
private Collection $childComments;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $score = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 32)]
|
||||||
|
private ?string $state = null;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?DateTimeImmutable $createdAt = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
|
||||||
|
private ?DateTimeInterface $updatedAt = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
|
||||||
|
private ?DateTimeInterface $deletedAt = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne]
|
||||||
|
private ?User $deletedByUser = null;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->childComments = new ArrayCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUser(): ?User
|
||||||
|
{
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUser(?User $user): static
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMarkdown(): ?string
|
||||||
|
{
|
||||||
|
return $this->markdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMarkdown(string $markdown): static
|
||||||
|
{
|
||||||
|
$this->markdown = $markdown;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHtml(): ?string
|
||||||
|
{
|
||||||
|
return $this->html;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setHtml(string $html): static
|
||||||
|
{
|
||||||
|
$this->html = $html;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getParentComment(): ?self
|
||||||
|
{
|
||||||
|
return $this->parentComment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setParentComment(?self $parentComment): static
|
||||||
|
{
|
||||||
|
$this->parentComment = $parentComment;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, self>
|
||||||
|
*/
|
||||||
|
public function getChildComments(): Collection
|
||||||
|
{
|
||||||
|
return $this->childComments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addChildComment(self $childComment): static
|
||||||
|
{
|
||||||
|
if (!$this->childComments->contains($childComment)) {
|
||||||
|
$this->childComments->add($childComment);
|
||||||
|
$childComment->setParentComment($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeChildComment(self $childComment): static
|
||||||
|
{
|
||||||
|
if ($this->childComments->removeElement($childComment)) {
|
||||||
|
// set the owning side to null (unless already changed)
|
||||||
|
if ($childComment->getParentComment() === $this) {
|
||||||
|
$childComment->setParentComment(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getScore(): ?int
|
||||||
|
{
|
||||||
|
return $this->score;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setScore(int $score): static
|
||||||
|
{
|
||||||
|
$this->score = $score;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getState(): ?string
|
||||||
|
{
|
||||||
|
return $this->state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setState(string $state): static
|
||||||
|
{
|
||||||
|
$this->state = $state;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): ?DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCreatedAt(DateTimeImmutable $createdAt): static
|
||||||
|
{
|
||||||
|
$this->createdAt = $createdAt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdatedAt(): ?DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUpdatedAt(?DateTimeInterface $updatedAt): static
|
||||||
|
{
|
||||||
|
$this->updatedAt = $updatedAt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDeletedAt(): ?DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->deletedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDeletedAt(?DateTimeInterface $deletedAt): static
|
||||||
|
{
|
||||||
|
$this->deletedAt = $deletedAt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDeletedByUser(): ?User
|
||||||
|
{
|
||||||
|
return $this->deletedByUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDeletedByUser(?User $deletedByUser): static
|
||||||
|
{
|
||||||
|
$this->deletedByUser = $deletedByUser;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPage(): ?DomainPage
|
||||||
|
{
|
||||||
|
return $this->page;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPage(?DomainPage $page): static
|
||||||
|
{
|
||||||
|
$this->page = $page;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,9 @@
|
|||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
use App\Repository\DomainRepository;
|
use App\Repository\DomainRepository;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Gedmo\Mapping\Annotation\Timestampable;
|
use Gedmo\Mapping\Annotation\Timestampable;
|
||||||
@ -32,11 +35,23 @@ class Domain
|
|||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
|
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
|
||||||
#[Timestampable(on: "update")]
|
#[Timestampable(on: "update")]
|
||||||
private ?\DateTimeInterface $updatedAt = null;
|
private ?DateTimeInterface $updatedAt = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
|
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
|
||||||
#[Timestampable(on: "create")]
|
#[Timestampable(on: "create")]
|
||||||
private ?\DateTimeInterface $createdAt = null;
|
private ?DateTimeInterface $createdAt = null;
|
||||||
|
|
||||||
|
#[ORM\OneToMany(mappedBy: 'domain', targetEntity: Comment::class)]
|
||||||
|
private Collection $comments;
|
||||||
|
|
||||||
|
#[ORM\OneToMany(mappedBy: 'domain', targetEntity: DomainPage::class)]
|
||||||
|
private Collection $domainPages;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->comments = new ArrayCollection();
|
||||||
|
$this->domainPages = new ArrayCollection();
|
||||||
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
{
|
{
|
||||||
@ -103,27 +118,87 @@ class Domain
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUpdatedAt(): ?\DateTimeInterface
|
public function getUpdatedAt(): ?DateTimeInterface
|
||||||
{
|
{
|
||||||
return $this->updatedAt;
|
return $this->updatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setUpdatedAt(?\DateTimeInterface $updatedAt): static
|
public function setUpdatedAt(?DateTimeInterface $updatedAt): static
|
||||||
{
|
{
|
||||||
$this->updatedAt = $updatedAt;
|
$this->updatedAt = $updatedAt;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCreatedAt(): ?\DateTimeInterface
|
public function getCreatedAt(): ?DateTimeInterface
|
||||||
{
|
{
|
||||||
return $this->createdAt;
|
return $this->createdAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setCreatedAt(\DateTimeInterface $createdAt): static
|
public function setCreatedAt(DateTimeInterface $createdAt): static
|
||||||
{
|
{
|
||||||
$this->createdAt = $createdAt;
|
$this->createdAt = $createdAt;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, Comment>
|
||||||
|
*/
|
||||||
|
public function getComments(): Collection
|
||||||
|
{
|
||||||
|
return $this->comments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addComment(Comment $comment): static
|
||||||
|
{
|
||||||
|
if (!$this->comments->contains($comment)) {
|
||||||
|
$this->comments->add($comment);
|
||||||
|
$comment->setDomain($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeComment(Comment $comment): static
|
||||||
|
{
|
||||||
|
if ($this->comments->removeElement($comment)) {
|
||||||
|
// set the owning side to null (unless already changed)
|
||||||
|
if ($comment->getDomain() === $this) {
|
||||||
|
$comment->setDomain(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, DomainPage>
|
||||||
|
*/
|
||||||
|
public function getDomainPages(): Collection
|
||||||
|
{
|
||||||
|
return $this->domainPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addDomainPage(DomainPage $domainPage): static
|
||||||
|
{
|
||||||
|
if (!$this->domainPages->contains($domainPage)) {
|
||||||
|
$this->domainPages->add($domainPage);
|
||||||
|
$domainPage->setDomain($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeDomainPage(DomainPage $domainPage): static
|
||||||
|
{
|
||||||
|
if ($this->domainPages->removeElement($domainPage)) {
|
||||||
|
// set the owning side to null (unless already changed)
|
||||||
|
if ($domainPage->getDomain() === $this) {
|
||||||
|
$domainPage->setDomain(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
184
src/Entity/DomainPage.php
Normal file
184
src/Entity/DomainPage.php
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\DomainPageRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: DomainPageRepository::class)]
|
||||||
|
class DomainPage
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(inversedBy: 'domainPages')]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
private ?Domain $domain = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT)]
|
||||||
|
private ?string $path = null;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?bool $locked = null;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $commentCount = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT)]
|
||||||
|
private ?string $title = null;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?DateTimeImmutable $createdAt = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
|
||||||
|
private ?DateTimeInterface $updatedAt = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
|
||||||
|
private ?DateTimeInterface $deletedAt = null;
|
||||||
|
|
||||||
|
#[ORM\OneToMany(mappedBy: 'page', targetEntity: Comment::class)]
|
||||||
|
private Collection $comments;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->comments = new ArrayCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDomain(): ?Domain
|
||||||
|
{
|
||||||
|
return $this->domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDomain(?Domain $domain): static
|
||||||
|
{
|
||||||
|
$this->domain = $domain;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPath(): ?string
|
||||||
|
{
|
||||||
|
return $this->path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPath(string $path): static
|
||||||
|
{
|
||||||
|
$this->path = $path;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isLocked(): ?bool
|
||||||
|
{
|
||||||
|
return $this->locked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLocked(bool $locked): static
|
||||||
|
{
|
||||||
|
$this->locked = $locked;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCommentCount(): ?int
|
||||||
|
{
|
||||||
|
return $this->commentCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCommentCount(int $commentCount): static
|
||||||
|
{
|
||||||
|
$this->commentCount = $commentCount;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): ?string
|
||||||
|
{
|
||||||
|
return $this->title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTitle(string $title): static
|
||||||
|
{
|
||||||
|
$this->title = $title;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): ?DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCreatedAt(DateTimeImmutable $createdAt): static
|
||||||
|
{
|
||||||
|
$this->createdAt = $createdAt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdatedAt(): ?DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUpdatedAt(?DateTimeInterface $updatedAt): static
|
||||||
|
{
|
||||||
|
$this->updatedAt = $updatedAt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDeletedAt(): ?DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->deletedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDeletedAt(?DateTimeInterface $deletedAt): static
|
||||||
|
{
|
||||||
|
$this->deletedAt = $deletedAt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, Comment>
|
||||||
|
*/
|
||||||
|
public function getComments(): Collection
|
||||||
|
{
|
||||||
|
return $this->comments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addComment(Comment $comment): static
|
||||||
|
{
|
||||||
|
if (!$this->comments->contains($comment)) {
|
||||||
|
$this->comments->add($comment);
|
||||||
|
$comment->setPage($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeComment(Comment $comment): static
|
||||||
|
{
|
||||||
|
if ($this->comments->removeElement($comment)) {
|
||||||
|
// set the owning side to null (unless already changed)
|
||||||
|
if ($comment->getPage() === $this) {
|
||||||
|
$comment->setPage(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
67
src/Entity/PageView.php
Normal file
67
src/Entity/PageView.php
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\PageViewRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: PageViewRepository::class)]
|
||||||
|
class PageView
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
private ?DomainPage $page = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne]
|
||||||
|
private ?User $user = null;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?DateTimeImmutable $createdAt = null;
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPage(): ?DomainPage
|
||||||
|
{
|
||||||
|
return $this->page;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPage(?DomainPage $page): static
|
||||||
|
{
|
||||||
|
$this->page = $page;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUser(): ?User
|
||||||
|
{
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUser(?User $user): static
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): ?DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCreatedAt(DateTimeImmutable $createdAt): static
|
||||||
|
{
|
||||||
|
$this->createdAt = $createdAt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
@ -3,22 +3,44 @@
|
|||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
use App\Repository\UserRepository;
|
use App\Repository\UserRepository;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Gedmo\Mapping\Annotation\Timestampable;
|
||||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
||||||
#[ORM\Table(name: '`user`')]
|
#[ORM\Table(name: '`user`')]
|
||||||
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||||
{
|
{
|
||||||
|
const ROLE_USER = 'ROLE_USER';
|
||||||
|
const ROLE_OWNER = 'ROLE_OWNER';
|
||||||
|
const ROLE_ADMIN = 'ROLE_ADMIN';
|
||||||
|
const ROLE_SUPER_ADMIN = 'ROLE_SUPER_ADMIN';
|
||||||
|
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 180, unique: true)]
|
#[ORM\Column(length: 180, unique: true)]
|
||||||
|
#[Assert\NotBlank]
|
||||||
|
#[Assert\Email(mode: Assert\Email::VALIDATION_MODE_HTML5)]
|
||||||
|
#[Assert\Length(max: 180)]
|
||||||
|
#[Assert\NoSuspiciousCharacters()]
|
||||||
private ?string $email = null;
|
private ?string $email = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 64)]
|
||||||
|
#[Assert\NotBlank]
|
||||||
|
#[Assert\Length(max: 64)]
|
||||||
|
#[Assert\NoSuspiciousCharacters()]
|
||||||
|
#[Assert\Regex(pattern: "/^[a-zA-Z0-9 _\-]+$/", message: "Display name can only contain characters [a-zA-Z0-9 _-]")]
|
||||||
|
private ?string $displayName = null;
|
||||||
|
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
private array $roles = [];
|
private array $roles = [];
|
||||||
|
|
||||||
@ -28,6 +50,22 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
private ?string $password = null;
|
private ?string $password = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
|
||||||
|
#[Timestampable(on: "create")]
|
||||||
|
private ?DateTimeInterface $createdAt = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
|
||||||
|
#[Timestampable(on: "update")]
|
||||||
|
private ?DateTimeInterface $updatedAt = null;
|
||||||
|
|
||||||
|
#[ORM\OneToMany(mappedBy: 'user', targetEntity: Comment::class)]
|
||||||
|
private Collection $comments;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->comments = new ArrayCollection();
|
||||||
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
@ -45,6 +83,18 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getDisplayName(): ?string
|
||||||
|
{
|
||||||
|
return $this->displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDisplayName(string $displayName): static
|
||||||
|
{
|
||||||
|
$this->displayName = $displayName;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A visual identifier that represents this user.
|
* A visual identifier that represents this user.
|
||||||
*
|
*
|
||||||
@ -74,6 +124,19 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addRole(string $role): static
|
||||||
|
{
|
||||||
|
if(!$this->roles) {
|
||||||
|
$this->roles = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!in_array($role, $this->roles)) {
|
||||||
|
$this->roles[] = $role;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see PasswordAuthenticatedUserInterface
|
* @see PasswordAuthenticatedUserInterface
|
||||||
*/
|
*/
|
||||||
@ -97,4 +160,58 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
// If you store any temporary, sensitive data on the user, clear it here
|
// If you store any temporary, sensitive data on the user, clear it here
|
||||||
// $this->plainPassword = null;
|
// $this->plainPassword = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): ?DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCreatedAt(DateTimeInterface $createdAt): static
|
||||||
|
{
|
||||||
|
$this->createdAt = $createdAt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdatedAt(): ?DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUpdatedAt(?DateTimeInterface $updatedAt): static
|
||||||
|
{
|
||||||
|
$this->updatedAt = $updatedAt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, Comment>
|
||||||
|
*/
|
||||||
|
public function getComments(): Collection
|
||||||
|
{
|
||||||
|
return $this->comments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addComment(Comment $comment): static
|
||||||
|
{
|
||||||
|
if (!$this->comments->contains($comment)) {
|
||||||
|
$this->comments->add($comment);
|
||||||
|
$comment->setUser($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeComment(Comment $comment): static
|
||||||
|
{
|
||||||
|
if ($this->comments->removeElement($comment)) {
|
||||||
|
// set the owning side to null (unless already changed)
|
||||||
|
if ($comment->getUser() === $this) {
|
||||||
|
$comment->setUser(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
48
src/Repository/CommentRepository.php
Normal file
48
src/Repository/CommentRepository.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\Comment;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<Comment>
|
||||||
|
*
|
||||||
|
* @method Comment|null find($id, $lockMode = null, $lockVersion = null)
|
||||||
|
* @method Comment|null findOneBy(array $criteria, array $orderBy = null)
|
||||||
|
* @method Comment[] findAll()
|
||||||
|
* @method Comment[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||||
|
*/
|
||||||
|
class CommentRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, Comment::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * @return Comment[] Returns an array of Comment objects
|
||||||
|
// */
|
||||||
|
// public function findByExampleField($value): array
|
||||||
|
// {
|
||||||
|
// return $this->createQueryBuilder('c')
|
||||||
|
// ->andWhere('c.exampleField = :val')
|
||||||
|
// ->setParameter('val', $value)
|
||||||
|
// ->orderBy('c.id', 'ASC')
|
||||||
|
// ->setMaxResults(10)
|
||||||
|
// ->getQuery()
|
||||||
|
// ->getResult()
|
||||||
|
// ;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public function findOneBySomeField($value): ?Comment
|
||||||
|
// {
|
||||||
|
// return $this->createQueryBuilder('c')
|
||||||
|
// ->andWhere('c.exampleField = :val')
|
||||||
|
// ->setParameter('val', $value)
|
||||||
|
// ->getQuery()
|
||||||
|
// ->getOneOrNullResult()
|
||||||
|
// ;
|
||||||
|
// }
|
||||||
|
}
|
48
src/Repository/DomainPageRepository.php
Normal file
48
src/Repository/DomainPageRepository.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\DomainPage;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<DomainPage>
|
||||||
|
*
|
||||||
|
* @method DomainPage|null find($id, $lockMode = null, $lockVersion = null)
|
||||||
|
* @method DomainPage|null findOneBy(array $criteria, array $orderBy = null)
|
||||||
|
* @method DomainPage[] findAll()
|
||||||
|
* @method DomainPage[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||||
|
*/
|
||||||
|
class DomainPageRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, DomainPage::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * @return DomainPage[] Returns an array of DomainPage objects
|
||||||
|
// */
|
||||||
|
// public function findByExampleField($value): array
|
||||||
|
// {
|
||||||
|
// return $this->createQueryBuilder('d')
|
||||||
|
// ->andWhere('d.exampleField = :val')
|
||||||
|
// ->setParameter('val', $value)
|
||||||
|
// ->orderBy('d.id', 'ASC')
|
||||||
|
// ->setMaxResults(10)
|
||||||
|
// ->getQuery()
|
||||||
|
// ->getResult()
|
||||||
|
// ;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public function findOneBySomeField($value): ?DomainPage
|
||||||
|
// {
|
||||||
|
// return $this->createQueryBuilder('d')
|
||||||
|
// ->andWhere('d.exampleField = :val')
|
||||||
|
// ->setParameter('val', $value)
|
||||||
|
// ->getQuery()
|
||||||
|
// ->getOneOrNullResult()
|
||||||
|
// ;
|
||||||
|
// }
|
||||||
|
}
|
48
src/Repository/PageViewRepository.php
Normal file
48
src/Repository/PageViewRepository.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\PageView;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<PageView>
|
||||||
|
*
|
||||||
|
* @method PageView|null find($id, $lockMode = null, $lockVersion = null)
|
||||||
|
* @method PageView|null findOneBy(array $criteria, array $orderBy = null)
|
||||||
|
* @method PageView[] findAll()
|
||||||
|
* @method PageView[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||||
|
*/
|
||||||
|
class PageViewRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, PageView::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * @return PageView[] Returns an array of PageView objects
|
||||||
|
// */
|
||||||
|
// public function findByExampleField($value): array
|
||||||
|
// {
|
||||||
|
// return $this->createQueryBuilder('p')
|
||||||
|
// ->andWhere('p.exampleField = :val')
|
||||||
|
// ->setParameter('val', $value)
|
||||||
|
// ->orderBy('p.id', 'ASC')
|
||||||
|
// ->setMaxResults(10)
|
||||||
|
// ->getQuery()
|
||||||
|
// ->getResult()
|
||||||
|
// ;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public function findOneBySomeField($value): ?PageView
|
||||||
|
// {
|
||||||
|
// return $this->createQueryBuilder('p')
|
||||||
|
// ->andWhere('p.exampleField = :val')
|
||||||
|
// ->setParameter('val', $value)
|
||||||
|
// ->getQuery()
|
||||||
|
// ->getOneOrNullResult()
|
||||||
|
// ;
|
||||||
|
// }
|
||||||
|
}
|
73
src/Utils/UserValidator.php
Normal file
73
src/Utils/UserValidator.php
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Utils;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||||
|
use function Symfony\Component\String\u;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to provide an example of integrating simple classes as
|
||||||
|
* services into a Symfony application.
|
||||||
|
* See https://symfony.com/doc/current/service_container.html#creating-configuring-services-in-the-container.
|
||||||
|
*
|
||||||
|
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||||
|
*/
|
||||||
|
final class UserValidator
|
||||||
|
{
|
||||||
|
public function validateDisplayName(?string $displayName): string
|
||||||
|
{
|
||||||
|
if (empty($displayName)) {
|
||||||
|
throw new InvalidArgumentException('The display name can not be empty.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (1 !== preg_match('/^[0-9a-zA-Z _\-]+$/', $displayName)) {
|
||||||
|
throw new InvalidArgumentException('The display name must contain only latin characters and underscores.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validatePassword(?string $plainPassword): string
|
||||||
|
{
|
||||||
|
if (empty($plainPassword)) {
|
||||||
|
throw new InvalidArgumentException('The password can not be empty.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (u($plainPassword)->trim()->length() < 6) {
|
||||||
|
throw new InvalidArgumentException('The password must be at least 6 characters long.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $plainPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateEmail(?string $email): string
|
||||||
|
{
|
||||||
|
if (empty($email)) {
|
||||||
|
throw new InvalidArgumentException('The email can not be empty.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === u($email)->indexOf('@')) {
|
||||||
|
throw new InvalidArgumentException('The email should look like a real email.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateFullName(?string $fullName): string
|
||||||
|
{
|
||||||
|
if (empty($fullName)) {
|
||||||
|
throw new InvalidArgumentException('The full name can not be empty.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $fullName;
|
||||||
|
}
|
||||||
|
}
|
30
symfony.lock
30
symfony.lock
@ -213,6 +213,20 @@
|
|||||||
"config/packages/security.yaml"
|
"config/packages/security.yaml"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"symfony/stimulus-bundle": {
|
||||||
|
"version": "2.11",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "2.9",
|
||||||
|
"ref": "05c45071c7ecacc1e48f94bc43c1f8d4405fb2b2"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"assets/bootstrap.js",
|
||||||
|
"assets/controllers.json",
|
||||||
|
"assets/controllers/hello_controller.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
"symfony/translation": {
|
"symfony/translation": {
|
||||||
"version": "6.3",
|
"version": "6.3",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
@ -276,6 +290,22 @@
|
|||||||
"config/packages/messenger.yaml"
|
"config/packages/messenger.yaml"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"symfony/webpack-encore-bundle": {
|
||||||
|
"version": "2.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "2.0",
|
||||||
|
"ref": "082d754b3bd54b3fc669f278f1eea955cfd23cf5"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"assets/app.js",
|
||||||
|
"assets/styles/app.css",
|
||||||
|
"config/packages/webpack_encore.yaml",
|
||||||
|
"package.json",
|
||||||
|
"webpack.config.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
"twig/extra-bundle": {
|
"twig/extra-bundle": {
|
||||||
"version": "v3.7.1"
|
"version": "v3.7.1"
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,213 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html data-bs-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{% block title %}Welcome!{% endblock %}</title>
|
<title>{% block title %}Welcome!{% endblock %}</title>
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text></svg>">
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text></svg>">
|
||||||
{% block stylesheets %}
|
{% block stylesheets %}
|
||||||
|
{{ encore_entry_link_tags('app') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block extra_stylesheets %}{% endblock %}
|
||||||
|
|
||||||
{% block javascripts %}
|
{% block javascripts %}
|
||||||
|
{{ encore_entry_script_tags('app') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block extra_javascripts %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{% block body %}{% endblock %}
|
{% if is_granted('ROLE_USER') %}
|
||||||
|
{# Main Navigation #}
|
||||||
|
<header>
|
||||||
|
{# Sidebar #}
|
||||||
|
<nav id="sidebarMenu" class="collapse d-lg-block sidebar collapse bg-body-tertiary">
|
||||||
|
<div class="position-sticky">
|
||||||
|
<div class="list-group list-group-flush mx-3 mt-4">
|
||||||
|
<a href="#" class="list-group-item list-group-item-action py-2 ripple{{ app.current_route() == 'app_dashboard' ? ' active' : '' }}" aria-current="true">
|
||||||
|
<i class="fas fa-tachometer-alt fa-fw me-3"></i>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="list-group-item list-group-item-action py-2 ripple">
|
||||||
|
<i class="fas fa-comments fa-fw me-3"></i>
|
||||||
|
<span>Comments</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="list-group-item list-group-item-action py-2 ripple">
|
||||||
|
<i class="fas fa-globe fa-fw me-3"></i>
|
||||||
|
<span>Domains</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="list-group-item list-group-item-action py-2 ripple">
|
||||||
|
<i class="fas fa-users fa-fw me-3"></i>
|
||||||
|
<span>Users</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="list-group-item list-group-item-action py-2 ripple">
|
||||||
|
<i class="fas fa-chart-bar fa-fw me-3"></i>
|
||||||
|
<span>Stats</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
{# Sidebar #}
|
||||||
|
|
||||||
|
{# Navbar #}
|
||||||
|
<nav id="main-navbar" class="navbar navbar-expand-lg navbar-light bg-body-secondary fixed-top">
|
||||||
|
{# Container wrapper #}
|
||||||
|
<div class="container-fluid">
|
||||||
|
{# Toggle button #}
|
||||||
|
<button
|
||||||
|
class="navbar-toggler"
|
||||||
|
type="button"
|
||||||
|
data-mdb-toggle="collapse"
|
||||||
|
data-mdb-target="#sidebarMenu"
|
||||||
|
aria-controls="sidebarMenu"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-label="Toggle navigation"
|
||||||
|
>
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{# Brand #}
|
||||||
|
<a class="navbar-brand" href="#">
|
||||||
|
MyComments
|
||||||
|
</a>
|
||||||
|
{# Search form #}
|
||||||
|
{# <form class="d-none d-md-flex input-group w-auto my-auto">#}
|
||||||
|
{# <input#}
|
||||||
|
{# autocomplete="off"#}
|
||||||
|
{# type="search"#}
|
||||||
|
{# class="form-control rounded"#}
|
||||||
|
{# placeholder='Search (ctrl + "/" to focus)'#}
|
||||||
|
{# style="min-width: 225px"#}
|
||||||
|
{# />#}
|
||||||
|
{# <span class="input-group-text border-0"#}
|
||||||
|
{# ><i class="fas fa-search"></i#}
|
||||||
|
{# ></span>#}
|
||||||
|
{# </form>#}
|
||||||
|
|
||||||
|
{# Right links #}
|
||||||
|
<ul class="navbar-nav ms-auto d-flex flex-row">
|
||||||
|
{# Notification dropdown #}
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a
|
||||||
|
class="nav-link me-3 px-3 me-lg-0 dropdown-toggle hidden-arrow"
|
||||||
|
href="#"
|
||||||
|
id="navbarDropdownMenuLink"
|
||||||
|
role="button"
|
||||||
|
data-mdb-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<i class="fas fa-bell"></i>
|
||||||
|
<span class="badge rounded-pill badge-notification bg-danger"
|
||||||
|
>1</span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
<ul
|
||||||
|
class="dropdown-menu dropdown-menu-end"
|
||||||
|
aria-labelledby="navbarDropdownMenuLink"
|
||||||
|
>
|
||||||
|
<li><a class="dropdown-item" href="#">Some news</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#">Another news</a></li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="#">Something else here</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{# Icon #}
|
||||||
|
{# <li class="nav-item me-3 me-lg-0">#}
|
||||||
|
{# <a class="nav-link" href="#">#}
|
||||||
|
{# <i class="fab fa-github"></i>#}
|
||||||
|
{# </a>#}
|
||||||
|
{# </li>#}
|
||||||
|
|
||||||
|
{# Icon dropdown #}
|
||||||
|
{# <li class="nav-item dropdown">#}
|
||||||
|
{# <a#}
|
||||||
|
{# class="nav-link me-3 me-lg-0 dropdown-toggle hidden-arrow"#}
|
||||||
|
{# href="#"#}
|
||||||
|
{# id="navbarDropdown"#}
|
||||||
|
{# role="button"#}
|
||||||
|
{# data-mdb-toggle="dropdown"#}
|
||||||
|
{# aria-expanded="false"#}
|
||||||
|
{# >#}
|
||||||
|
{# <i class="united kingdom flag m-0"></i>#}
|
||||||
|
{# </a>#}
|
||||||
|
{# <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">#}
|
||||||
|
{# <li>#}
|
||||||
|
{# <a class="dropdown-item" href="#">#}
|
||||||
|
{# <i class="united kingdom flag"></i>English <i class="fa fa-check text-success ms-2"></i>#}
|
||||||
|
{# </a>#}
|
||||||
|
{# </li>#}
|
||||||
|
{# <li><hr class="dropdown-divider" /></li>#}
|
||||||
|
{# <li>#}
|
||||||
|
{# <a class="dropdown-item" href="#"><i class="poland flag"></i>Polski</a>#}
|
||||||
|
{# </li>#}
|
||||||
|
{# <li>#}
|
||||||
|
{# <a class="dropdown-item" href="#"><i class="china flag"></i>中文</a>#}
|
||||||
|
{# </li>#}
|
||||||
|
{# <li>#}
|
||||||
|
{# <a class="dropdown-item" href="#"><i class="japan flag"></i>日本語</a>#}
|
||||||
|
{# </li>#}
|
||||||
|
{# <li>#}
|
||||||
|
{# <a class="dropdown-item" href="#"><i class="germany flag"></i>Deutsch</a>#}
|
||||||
|
{# </li>#}
|
||||||
|
{# <li>#}
|
||||||
|
{# <a class="dropdown-item" href="#"><i class="france flag"></i>Français</a>#}
|
||||||
|
{# </li>#}
|
||||||
|
{# <li>#}
|
||||||
|
{# <a class="dropdown-item" href="#"><i class="spain flag"></i>Español</a>#}
|
||||||
|
{# </li>#}
|
||||||
|
{# <li>#}
|
||||||
|
{# <a class="dropdown-item" href="#"><i class="russia flag"></i>Русский</a>#}
|
||||||
|
{# </li>#}
|
||||||
|
{# <li>#}
|
||||||
|
{# <a class="dropdown-item" href="#"><i class="portugal flag"></i>Português</a>#}
|
||||||
|
{# </li>#}
|
||||||
|
{# </ul>#}
|
||||||
|
{# </li>#}
|
||||||
|
|
||||||
|
{# Avatar #}
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a
|
||||||
|
class="nav-link dropdown-toggle hidden-arrow d-flex align-items-center"
|
||||||
|
href="#"
|
||||||
|
id="navbarDropdownMenuLink"
|
||||||
|
role="button"
|
||||||
|
data-mdb-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="https://mdbootstrap.com/img/Photos/Avatars/img (31).jpg"
|
||||||
|
class="rounded-circle"
|
||||||
|
height="24"
|
||||||
|
alt=""
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<ul
|
||||||
|
class="dropdown-menu dropdown-menu-end"
|
||||||
|
aria-labelledby="navbarDropdownMenuLink"
|
||||||
|
>
|
||||||
|
<li><a class="dropdown-item" href="#">My profile</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#">Settings</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{{ url('app_logout') }}">Logout</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{# Container wrapper #}
|
||||||
|
</nav>
|
||||||
|
{# Navbar #}
|
||||||
|
</header>
|
||||||
|
{# Main Navigation #}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% block container %}
|
||||||
|
<main style="margin-top: 58px">
|
||||||
|
<div class="container pt-4">
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -3,18 +3,6 @@
|
|||||||
{% block title %}Hello DashboardController!{% endblock %}
|
{% block title %}Hello DashboardController!{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<style>
|
<h4>Dashboard</h4>
|
||||||
.example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
|
<hr />
|
||||||
.example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="example-wrapper">
|
|
||||||
<h1>Hello {{ controller_name }}! ✅</h1>
|
|
||||||
|
|
||||||
This friendly message is coming from:
|
|
||||||
<ul>
|
|
||||||
<li>Your controller at <code><a href="{{ '/home/skylar/Projects/MyComments/src/Controller/DashboardController.php'|file_link(0) }}">src/Controller/DashboardController.php</a></code></li>
|
|
||||||
<li>Your template at <code><a href="{{ '/home/skylar/Projects/MyComments/templates/dashboard/index.html.twig'|file_link(0) }}">templates/dashboard/index.html.twig</a></code></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,20 +1,53 @@
|
|||||||
{% extends 'base.html.twig' %}
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
{% block body %}
|
{% block container %}
|
||||||
{% if error %}
|
|
||||||
<div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<form action="{{ path('app_login') }}" method="post">
|
<section class="vh-100 gradient-custom">
|
||||||
<label for="username">Email:</label>
|
<div class="container py-5 h-100">
|
||||||
<input type="text" id="username" name="_username" value="{{ last_username }}">
|
<div class="row d-flex justify-content-center align-items-center h-100">
|
||||||
|
<div class="col-12 col-md-8 col-lg-6 col-xl-5">
|
||||||
|
<div class="card bg-dark text-white" style="border-radius: 1rem;">
|
||||||
|
<div class="card-body p-5 text-center">
|
||||||
|
|
||||||
<label for="password">Password:</label>
|
<div class="mb-md-5 mt-md-4 pb-5">
|
||||||
<input type="password" id="password" name="_password">
|
<form action="{{ path('app_login') }}" method="post">
|
||||||
|
|
||||||
{# If you want to control the URL the user is redirected to on success
|
<h2 class="fw-bold mb-5">MyComments Login</h2>
|
||||||
<input type="hidden" name="_target_path" value="/account"> #}
|
{% if error %}
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
{{ error.messageKey|trans(error.messageData, 'security') }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<button type="submit">login</button>
|
<div class="form-outline form-white mb-4">
|
||||||
</form>
|
<input type="email" id="typeEmailX" name="_username" class="form-control form-control-lg" value="{{ last_username }}" />
|
||||||
{% endblock %}
|
<label class="form-label" for="typeEmailX">Email</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-outline form-white mb-4">
|
||||||
|
<input type="password" id="typePasswordX" name="_password" class="form-control form-control-lg" />
|
||||||
|
<label class="form-label" for="typePasswordX">Password</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-outline-light btn-lg px-5" type="submit">Login</button>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-center text-center mt-4 pt-1">
|
||||||
|
<a href="#" class="text-white"><i class="fab fa-google fa-lg mx-1"></i></a>
|
||||||
|
<a href="#" class="text-white"><i class="fab fa-github fa-lg mx-1"></i></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="mb-0"><a href="#!" class="text-white-50 fw-bold">Forgot your password?</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
|
79
webpack.config.js
Normal file
79
webpack.config.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
const Encore = require('@symfony/webpack-encore');
|
||||||
|
|
||||||
|
// Manually configure the runtime environment if not already configured yet by the "encore" command.
|
||||||
|
// It's useful when you use tools that rely on webpack.config.js file.
|
||||||
|
if (!Encore.isRuntimeEnvironmentConfigured()) {
|
||||||
|
Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
|
||||||
|
}
|
||||||
|
|
||||||
|
Encore
|
||||||
|
// directory where compiled assets will be stored
|
||||||
|
.setOutputPath('public/build/')
|
||||||
|
// public path used by the web server to access the output path
|
||||||
|
.setPublicPath('/build')
|
||||||
|
// only needed for CDN's or subdirectory deploy
|
||||||
|
//.setManifestKeyPrefix('build/')
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ENTRY CONFIG
|
||||||
|
*
|
||||||
|
* Each entry will result in one JavaScript file (e.g. app.js)
|
||||||
|
* and one CSS file (e.g. app.css) if your JavaScript imports CSS.
|
||||||
|
*/
|
||||||
|
.addEntry('app', './assets/app.js')
|
||||||
|
|
||||||
|
// When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
|
||||||
|
.splitEntryChunks()
|
||||||
|
|
||||||
|
// enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js)
|
||||||
|
.enableStimulusBridge('./assets/controllers.json')
|
||||||
|
|
||||||
|
// will require an extra script tag for runtime.js
|
||||||
|
// but, you probably want this, unless you're building a single-page app
|
||||||
|
.enableSingleRuntimeChunk()
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FEATURE CONFIG
|
||||||
|
*
|
||||||
|
* Enable & configure other features below. For a full
|
||||||
|
* list of features, see:
|
||||||
|
* https://symfony.com/doc/current/frontend.html#adding-more-features
|
||||||
|
*/
|
||||||
|
.cleanupOutputBeforeBuild()
|
||||||
|
.enableBuildNotifications()
|
||||||
|
.enableSourceMaps(!Encore.isProduction())
|
||||||
|
// enables hashed filenames (e.g. app.abc123.css)
|
||||||
|
.enableVersioning(Encore.isProduction())
|
||||||
|
|
||||||
|
// configure Babel
|
||||||
|
// .configureBabel((config) => {
|
||||||
|
// config.plugins.push('@babel/a-babel-plugin');
|
||||||
|
// })
|
||||||
|
|
||||||
|
// enables and configure @babel/preset-env polyfills
|
||||||
|
.configureBabelPresetEnv((config) => {
|
||||||
|
config.useBuiltIns = 'usage';
|
||||||
|
config.corejs = '3.23';
|
||||||
|
})
|
||||||
|
|
||||||
|
// enables Sass/SCSS support
|
||||||
|
//.enableSassLoader()
|
||||||
|
|
||||||
|
// uncomment if you use TypeScript
|
||||||
|
//.enableTypeScriptLoader()
|
||||||
|
|
||||||
|
// uncomment if you use React
|
||||||
|
//.enableReactPreset()
|
||||||
|
|
||||||
|
// uncomment to get integrity="..." attributes on your script & link tags
|
||||||
|
// requires WebpackEncoreBundle 1.4 or higher
|
||||||
|
//.enableIntegrityHashes(Encore.isProduction())
|
||||||
|
|
||||||
|
// uncomment if you're having problems with a jQuery plugin
|
||||||
|
.autoProvidejQuery()
|
||||||
|
|
||||||
|
.enableSassLoader()
|
||||||
|
;
|
||||||
|
|
||||||
|
module.exports = Encore.getWebpackConfig();
|
Loading…
x
Reference in New Issue
Block a user