diff --git a/.env b/.env index 5b6040b..a0667ad 100644 --- a/.env +++ b/.env @@ -35,3 +35,10 @@ MAILER_SENDER=example@localhost ASSET_BASE_URL=null UMAMI_URL=null +MESSENGER_TRANSPORT_DSN=doctrine://default + +INFLUXDB_URL= +INFLUXDB_TOKEN= +INFLUXDB_BUCKET= +INFLUXDB_ORG= +INFLUXDB_DEBUG=1 diff --git a/.mage.yml.dist b/.mage.yml.dist index 3992365..1b75320 100644 --- a/.mage.yml.dist +++ b/.mage.yml.dist @@ -18,6 +18,7 @@ magephp: - "/var/cache/*" - "/var/log/*" - "/public/media" + - "/.secrets" hosts: - ssh_host on-deploy: @@ -25,3 +26,4 @@ magephp: - exec: { cmd: 'make doctrine-migration', desc: 'migration' } - exec: { cmd: 'php8.1 ./bin/console cache:warmup', desc: 'warmup' } - exec: { cmd: 'php8.1 ./bin/console cache:warmup', desc: 'warmup2' } + - exec: { cmd: './bin/messenger -a restart', desc: 'messenger' } diff --git a/.novops.yml b/.novops.yml new file mode 100644 index 0000000..a0a9c25 --- /dev/null +++ b/.novops.yml @@ -0,0 +1,39 @@ +environments: + build: + variables: + - name: MYSQLDUMP + value: + hvault_kv2: + mount: kv + path: deblan/deblan.io-murph + key: mysqldump + + deploy: + variables: + - name: SSH_USER + value: + hvault_kv2: + mount: kv + path: deblan/deblan.io-murph + key: ssh_user + + - name: SSH_HOST + value: + hvault_kv2: + mount: kv + path: deblan/deblan.io-murph + key: ssh_host + + - name: SSH_PRIV_KEY + value: + hvault_kv2: + mount: kv + path: deblan/deblan.io-murph + key: ssh_priv_key + + - name: APP_DIRECTORY + value: + hvault_kv2: + mount: kv + path: deblan/deblan.io-murph + key: app_directory diff --git a/.woodpecker.yml b/.woodpecker/build.yml similarity index 55% rename from .woodpecker.yml rename to .woodpecker/build.yml index 9145df0..4f785fe 100644 --- a/.woodpecker.yml +++ b/.woodpecker/build.yml @@ -1,53 +1,56 @@ -pipeline: - db-wait: +variables: + volumes: &volumes + - node_cache:/root/.npm + - /data/${CI_REPO}:/builds + +when: + event: [push, pull_request, tag, manual] + branch: [master, master-*, develop, develop-*, feature/*] + +steps: + "Wait the database": image: gitnet.fr/deblan/timeout:latest commands: - /bin/timeout -t 30 -v -c 'while true; do nc -z -v db 3306 2>&1 | grep succeeded && exit 0; sleep 0.5; done' - db-create: + "Create database": image: mariadb:10.3 - secrets: [mysqldump] + environment: + MYSQLDUMP: + from_secret: mysqldump commands: - mysql -hdb -uroot -proot -e "CREATE DATABASE app" - eval "$MYSQLDUMP" | mysql -hdb -uroot -proot app - when: - branch: [master, master-*, develop, develop-*] - app-config: + "Configure app": image: deblan/php:8.1 commands: - echo APP_ENV=prod >> .env.local - echo APP_SECRET=$(openssl rand -hex 32) >> .env.local - echo DATABASE_URL=mysql://root:root@db/app >> .env.local - when: - branch: [master, master-*, develop, develop-*] - php-composer: + "Installs PHP dependencies": image: deblan/php:8.1 commands: - apt-get update && apt-get -y install git - composer install --no-scripts - db-migrate: - image: deblan/php:8.1 + "Migrates database": + image: deblan/php:8.3 environment: - - PHP=php + PHP: php commands: - ./bin/doctrine-migrate - when: - branch: [master, master-*, develop, develop-*, feature/*] - app-jsroutes: - image: deblan/php:8.1 + "Generates JS routes": + image: deblan/php:8.3 commands: - php bin/console fos:js-routing:dump --format=json --target=public/js/fos_js_routes.json - when: - branch: [master, master-*, develop, develop-*, feature/*] - node-build: - image: node:16-alpine + "Build assets": + image: node:20-alpine environment: - - CPU_COUNT=3 + CPU_COUNT: 3 commands: - apk add --no-cache git - npm install -g svg2ttf ttf2eot ttf2woff2 @@ -58,32 +61,24 @@ pipeline: - test -f public/js/fos_js_routes.json || echo "{}" > public/js/fos_js_routes.json - npm run build - security-check: + "Check dependencies": image: gitnet.fr/deblan/osv-detector:v0.9 commands: - osv-detector composer.lock yarn.lock failure: ignore - app-deploy: - image: deblan/php:8.1 - secrets: [ssh_user, ssh_host, ssh_priv_key, app_directory] + "Build the cache": + image: deblan/mage + volumes: *volumes commands: - - apt-get update && apt-get -y install rsync openssh-client - - mkdir "$HOME/.ssh" - - echo "$SSH_PRIV_KEY" > "$HOME/.ssh/id_ed25519" - - chmod 700 "$HOME/.ssh" - - chmod 600 "$HOME/.ssh/id_ed25519" - - composer global require andres-montanez/magallanes - - cp .mage.yml.dist .mage.yml - - sed -i "s/ssh_user/$SSH_USER/g" .mage.yml - - sed -i "s/ssh_host/$SSH_HOST/g" .mage.yml - - sed -i "s#app_directory#$APP_DIRECTORY#g" .mage.yml - - /root/.config/composer/vendor/bin/mage deploy "$CI_BUILD_DEPLOY_TARGET" - when: - event: [deployment] + - cd /builds + - rsync -az "$CI_WORKSPACE/" "$CI_COMMIT_SHA" services: db: image: mariadb:10.3 environment: - - MARIADB_ROOT_PASSWORD=root + MARIADB_ROOT_PASSWORD: root + +volumes: + node_cache: diff --git a/.woodpecker/deploy.yml b/.woodpecker/deploy.yml new file mode 100644 index 0000000..e78ac67 --- /dev/null +++ b/.woodpecker/deploy.yml @@ -0,0 +1,33 @@ +variables: + volumes: &volumes + - /data/${CI_REPO}:/builds + +when: + event: [deployment] + +skip_clone: true + +steps: + "Deploy": + image: deblan/mage + environment: + SSH_PRIV_KEY: + from_secret: ssh_priv_key + SSH_USER: + from_secret: ssh_user + SSH_HOST: + from_secret: ssh_host + APP_DIRECTORY: + from_secret: app_directory + volumes: *volumes + commands: + - cd "/builds/$CI_COMMIT_SHA" + - mkdir "$HOME/.ssh" + - echo "$SSH_PRIV_KEY" > "$HOME/.ssh/id_ed25519" + - chmod 700 "$HOME/.ssh" + - chmod 600 "$HOME/.ssh/id_ed25519" + - cp .mage.yml.dist .mage.yml + - sed -i "s/ssh_user/$SSH_USER/g" .mage.yml + - sed -i "s/ssh_host/$SSH_HOST/g" .mage.yml + - sed -i "s#app_directory#$APP_DIRECTORY#g" .mage.yml + - mage deploy "$CI_PIPELINE_DEPLOY_TARGET" diff --git a/Makefile b/Makefile index e352647..9d32bcc 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,6 @@ COMPOSER ?= composer -PHP ?= php8.1 -SSH ?= ssh -YARN ?= yarn +PHP_BIN ?= php8.1 +YARN_BIN ?= yarn NPM_BIN ?= npm all: build @@ -20,10 +19,10 @@ js-routing: doctrine-migration clean: rm -fr var/cache/dev/* rm -fr var/cache/prod/* - $(PHP) bin/console + $(PHP_BIN) bin/console doctrine-migration: - PHP=$(PHP) ./bin/doctrine-migrate + PHP=$(PHP_BIN) ./bin/doctrine-migrate .ONESHELL: lint: diff --git a/assets/css/admin.scss b/assets/css/admin.scss index 00c0430..3025de7 100644 --- a/assets/css/admin.scss +++ b/assets/css/admin.scss @@ -1,57 +1,63 @@ @import "../../vendor/murph/murph-core/src/core/Resources/assets/css/admin.scss"; -@import "~simplemde/dist/simplemde.min.css"; - -.CodeMirror-fullscreen, .editor-toolbar.fullscreen { - z-index: 2000; -} +@import "@kangc/v-md-editor/lib/style/base-editor.css"; +@import "@kangc/v-md-editor/lib/theme/style/vuepress.css"; .ejs-link { - margin: 10px auto; - max-width: 80%; - border: 2px solid #333; - border-radius: 5px; + margin: 10px auto; + max-width: 80%; + border: 2px solid #333; + border-radius: 5px; - &--anchor { - display: block; - padding: 30px; + &--anchor { + display: block; + padding: 30px; + } + + &-content { + display: inline-block; + vertical-align: top; + + &--title { + font-weight: bold; } - &-content { - display: inline-block; - vertical-align: top; - - &--title { - font-weight: bold; - } - - &--description { - font-size: 15px; - } - - &--link { - padding-top: 10px; - font-size: 14px; - line-height: 20px; - } + &--description { + font-size: 15px; } - $image-size: 85px; - - &--anchor--with-image &-content { - width: calc(100% - $image-size - 5px); - padding-right: 25px; + &--link { + padding-top: 10px; + font-size: 14px; + line-height: 20px; } + } - &--image { - display: inline-block; - width: $image-size; - height: $image-size; - background-position: center center; - background-repeat: no-repeat; - background-size: cover; - } + $image-size: 85px; + + &--anchor--with-image &-content { + width: calc(100% - $image-size - 5px); + padding-right: 25px; + } + + &--image { + display: inline-block; + width: $image-size; + height: $image-size; + background-position: center center; + background-repeat: no-repeat; + background-size: cover; + } } .choices__list--dropdown { z-index: 3; } + +.v-md-editor { + border: 1px solid $input-border-color; + box-shadow: none; +} + +.v-md-editor--fullscreen { + z-index: 3000; +} diff --git a/assets/css/app.scss b/assets/css/app.scss index 81a8b03..40b8938 100644 --- a/assets/css/app.scss +++ b/assets/css/app.scss @@ -1,29 +1,26 @@ @tailwind base; + @tailwind components; + @tailwind utilities; @import "app/config"; -@import 'app/prism'; +@import "app/prism"; @import "app/typo"; @import "~tingle.js/src/tingle.css"; @font-face { font-family: "MainFont"; - src: - url('../fonts/ubuntu/ubuntu-light.woff2?20211108') format('woff2'), - url('../fonts/ubuntu/ubuntu-light.woff?20211108') format('woff'); - // url('../fonts/atkinson/WOFF2/Atkinson-Hyperlegible-Regular-102a.woff2?20220911') format('woff2'), - // url('../fonts/atkinson/WOFF/Atkinson-Hyperlegible-Regular-102.woff?20211108w20220911') format('woff'); + src: url("../fonts/ubuntu/ubuntu-light.woff2?20211108") format("woff2"), url("../fonts/ubuntu/ubuntu-light.woff?20211108") format("woff"); + + // url('../fonts/atkinson/WOFF2/Atkinson-Hyperlegible-Regular-102a.woff2?20220911') format('woff2'), + // url('../fonts/atkinson/WOFF/Atkinson-Hyperlegible-Regular-102.woff?20211108w20220911') format('woff'); } @font-face { font-family: "deblan-icon"; - src: url('../fonts/deblan/deblan-icon.eot?20211108'); - src: - url('../fonts/deblan/deblan-icon.woff2?20211108') format('woff2'), - url('../fonts/deblan/deblan-icon.woff?20211108') format('woff'), - url('../fonts/deblan/deblan-icon.ttf?20211108') format('truetype'); - + src: url("../fonts/deblan/deblan-icon.eot?20211108"); + src: url("../fonts/deblan/deblan-icon.woff2?20211108") format("woff2"), url("../fonts/deblan/deblan-icon.woff?20211108") format("woff"), url("../fonts/deblan/deblan-icon.ttf?20211108") format("truetype"); font-style: normal; font-weight: normal; text-rendering: optimizeLegibility; @@ -61,7 +58,7 @@ $dicons: coffee server search project share contact list response twitter diaspo } .text-right { - text-align: right; + text-align: right; } .list--inline { @@ -157,8 +154,9 @@ pre[class*="language-"] { height: 50px; overflow: hidden; display: inline-block; - margin-bottom: -19px;i + margin-bottom: -19px; + i &:active, &:focus { background: none; } @@ -173,6 +171,7 @@ pre[class*="language-"] { background: url(../images/Refresh_icon.svg); } } + // // &-captcha { // label { @@ -304,9 +303,11 @@ pre[class*="language-"] { 0% { background-position: 0 0%; } + 50% { background-position: 0 75%; } + 100% { background-position: 0 0%; } @@ -347,18 +348,20 @@ pre[class*="language-"] { width: 100%; height: 10px; background: rgb(0, 0, 0); - background: linear-gradient(180deg, rgba(0, 0, 0, 0.3) 0%, rgba(255,255,255,0) 100%); + background: linear-gradient(180deg, rgba(0, 0, 0, 0.3) 0%, rgba(255, 255, 255, 0) 100%); } @keyframes HeaderGradient { 0% { - background-position: 0 50% + background-position: 0 50%; } + 50% { - background-position: 100% 50% + background-position: 100% 50%; } + 100% { - background-position: 0 50% + background-position: 0 50%; } } @@ -411,6 +414,9 @@ pre[class*="language-"] { .h1 { font-weight: normal; font-size: 40px; + font-family: MainFont; + text-shadow: none; + color: hsla(0, 0%, 100%, 0.7); } .h3 { @@ -472,7 +478,7 @@ pre[class*="language-"] { } p a:not(.btn), ul:not(.btn-group) a:not(.btn) { - background: url('../images/link.svg') bottom left repeat-x; + background: url("../images/link.svg") bottom left repeat-x; padding-bottom: 5px; } @@ -503,6 +509,35 @@ pre[class*="language-"] { @include make-pre-code; } + &.mermaid { + box-shadow: none; + background: #343c53; + border: 0; + + text, tspan { + fill: #fff !important; + } + + .actor, .noteText { + text, tspan { + fill: #333 !important; + } + } + + .labelText { + fill: #333 !important; + } + + line, path { + stroke: #fff !important; + } + + svg { + display: block; + margin: auto; + } + } + &.with-title { margin-top: 0; border-top-right-radius: 0; @@ -513,11 +548,41 @@ pre[class*="language-"] { padding-bottom: 10px; padding-top: 10px; border: 1px solid $color-code-border; + // overflow: hidden; // // &:hover { // overflow: auto; // } + + } + } +} + +.post-author-wrapper { + .post-author-avatar { + vertical-align: top; + width: 100px; + padding: 10px; + display: inline-block; + } + + .post-author { + display: inline-block; + width: calc(100% - 100px); + padding: 10px 10px 10px 30px; + } + + @media screen and (max-width: 774px) { + .post-author-avatar { + display: block; + margin: auto; + } + + .post-author { + display: block; + width: 100%; + padding-right: 20px; } } } @@ -577,12 +642,14 @@ pre[class*="language-"] { @for $i from 1 through 6 { .review.offset-#{$i} { margin-left: 5% * $i - 1%; - width: 100% - ($i * 5%); + width: 100% - $i * 5%; } } .review { width: 100%; + max-width: calc($content-max-width - 60px - 2rem); + overflow: auto; .review-avatar, .review-avatar img { width: 60px; @@ -598,7 +665,7 @@ pre[class*="language-"] { border: 1px solid $color-hr-border; .review-content p { - margin-top: 0; + margin-top: 0; } > ul { @@ -663,7 +730,7 @@ pre[class*="language-"] { .code-window { height: 50px; - background: $color-code-title-background url('../images/window.svg') no-repeat center right; + background: $color-code-title-background url("../images/window.svg") no-repeat center right; padding-left: 15px; font-family: Monospace; color: #ccc; @@ -697,7 +764,8 @@ pre[class*="language-"] { width: 100%; height: 450px; background-position: center center; - background: #f2f2f2 url('../images/quick-post-load.png') no-repeat center center; + background: #f2f2f2 url("../images/quick-post-load.png") no-repeat center center; + // border: 2px solid $color-very-light-grey; border-bottom: 0; cursor: pointer; @@ -836,19 +904,7 @@ pre[class*="language-"] { border: 1px solid $color-white; } -$links: ( - twitter: #20b8ff, - rss: #fd9f13, - linkedin: #006699, - diaspora: #90b92e, - github: #8cc345, - code: #51d066, - mastodon: #2984d2, - pixelfed: #e72151, - matrix: #1a588a, - gpg: #42a73b, - murph: #19b4db -); +$links: (twitter: #20b8ff, rss: #fd9f13, linkedin: #006699, diaspora: #90b92e, github: #8cc345, code: #51d066, mastodon: #2984d2, pixelfed: #e72151, matrix: #1a588a, gpg: #42a73b, murph: #19b4db); @each $site, $bg in $links { .link-#{$site} { @@ -951,25 +1007,29 @@ $links: ( } @keyframes bounceIn { - 0%{ + 0% { opacity: 0; } - 50%{ + + 50% { opacity: 0.9; } - 80%{ + + 80% { opacity: 1; } - 100%{ + + 100% { opacity: 1; } } @keyframes knmc { - 0%{ - left: -256px;; + 0% { + left: -256px; } - 100%{ + + 100% { left: 150vw; } } @@ -1074,6 +1134,8 @@ $links: ( .ejs-link { margin: 10px auto; + width: 80%; + margin-bottom: 20px; &--anchor { display: block; @@ -1198,6 +1260,8 @@ $links: ( } .ejs-link { + width: auto; + &-content { display: block; width: 100% !important; @@ -1271,13 +1335,13 @@ $links: ( } .invalid-feedback { - color: red; - padding-left: 10px; - padding-right: 10px; + color: red; + padding-left: 10px; + padding-right: 10px; - .form-error-icon { - display: none; - } + .form-error-icon { + display: none; + } } @media (prefers-color-scheme: dark) { @@ -1315,7 +1379,7 @@ $links: ( } .header-shadow { - background: linear-gradient(180deg, rgba(0, 0, 0, 0.3) 0%, rgba(255,255,255,0) 100%); + background: linear-gradient(180deg, rgba(0, 0, 0, 0.3) 0%, rgba(255, 255, 255, 0) 100%); } } @@ -1328,3 +1392,18 @@ $links: ( } } } + +.deprecated { + color: #fff; + background: #3abff8; + padding: 1rem; + border-radius: var(--rounded-box, 1rem); + text-align: center; + + svg { + display: inline-block; + height: 25px; + vertical-align: top; + margin-right: 8px; + } +} diff --git a/assets/css/app/alert.scss b/assets/css/app/alert.scss index e4cb112..d63e072 100644 --- a/assets/css/app/alert.scss +++ b/assets/css/app/alert.scss @@ -1,34 +1,34 @@ .alert { - padding: 20px; - border: 1px solid #333; + padding: 20px; + border: 1px solid #333; - &-success { - border-color: #9db024; - background: #c6ff69; - color: #415f29; - } + &-success { + border-color: #9db024; + background: #c6ff69; + color: #415f29; + } - &-notice { - border-color: $color-blue; - background: #66e6ff; - color: #254e5f; - } + &-notice { + border-color: $color-blue; + background: #66e6ff; + color: #254e5f; + } - &-warning { - border-color: #b07f29; - background: #ffd465; - color: #5f4520; - } + &-warning { + border-color: #b07f29; + background: #ffd465; + color: #5f4520; + } - &-error { - border-color: #b02e2a; - background: #ff6363; - color: #5f2521; - } + &-error { + border-color: #b02e2a; + background: #ff6363; + color: #5f2521; + } - &-notice-light { - border-color: $color-blue; - background: #d9fffc; - color: #254e5f; - } + &-notice-light { + border-color: $color-blue; + background: #d9fffc; + color: #254e5f; + } } diff --git a/assets/css/app/config.scss b/assets/css/app/config.scss index 9ed0c22..a633f7c 100644 --- a/assets/css/app/config.scss +++ b/assets/css/app/config.scss @@ -62,6 +62,7 @@ $color-code-text: #f8f8f2; $color-code-mark-background: $color-light-blue; $color-code-title-background: #1d2231; $color-code-title-text: #e0e0e0; + /* --- */ :root { diff --git a/assets/css/app/prism.scss b/assets/css/app/prism.scss index 1def76e..07f5199 100644 --- a/assets/css/app/prism.scss +++ b/assets/css/app/prism.scss @@ -8,61 +8,59 @@ https://prismjs.com/download.html#themes=prism-okaidia&languages=markup+css+clik code[class*="language-"], pre[class*="language-"] { - color: #f8f8f2; - background: none; - text-shadow: 0 1px rgba(0, 0, 0, 0.3); - font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; - font-size: 1em; - text-align: left; - white-space: pre; - word-spacing: normal; - word-break: normal; - word-wrap: normal; - line-height: 1.5; - - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; - - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; + color: #f8f8f2; + background: none; + text-shadow: 0 1px rgba(0, 0, 0, 0.3); + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + font-size: 1em; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; } /* Code blocks */ pre[class*="language-"] { - padding: 1em; - margin: .5em 0; - overflow: auto; - border-radius: 0.3em; + padding: 1em; + margin: .5em 0; + overflow: auto; + border-radius: 0.3em; } :not(pre) > code[class*="language-"], pre[class*="language-"] { - background: #272822; + background: #272822; } /* Inline code */ :not(pre) > code[class*="language-"] { - padding: .1em; - border-radius: .3em; - white-space: normal; + padding: .1em; + border-radius: .3em; + white-space: normal; } .token.comment, .token.prolog, .token.doctype, .token.cdata { - color: #8292a2; + color: #8292a2; } .token.punctuation { - color: #f8f8f2; + color: #f8f8f2; } .token.namespace { - opacity: .7; + opacity: .7; } .token.property, @@ -70,12 +68,12 @@ pre[class*="language-"] { .token.constant, .token.symbol, .token.deleted { - color: #f92672; + color: #f92672; } .token.boolean, .token.number { - color: #ae81ff; + color: #ae81ff; } .token.selector, @@ -84,7 +82,7 @@ pre[class*="language-"] { .token.char, .token.builtin, .token.inserted { - color: #a6e22e; + color: #a6e22e; } .token.operator, @@ -93,34 +91,34 @@ pre[class*="language-"] { .language-css .token.string, .style .token.string, .token.variable { - color: #f8f8f2; + color: #f8f8f2; } .token.atrule, .token.attr-value, .token.function, .token.class-name { - color: #e6db74; + color: #e6db74; } .token.keyword { - color: #66d9ef; + color: #66d9ef; } .token.regex, .token.important { - color: #fd971f; + color: #fd971f; } .token.important, .token.bold { - font-weight: bold; + font-weight: bold; } + .token.italic { - font-style: italic; + font-style: italic; } .token.entity { - cursor: help; + cursor: help; } - diff --git a/assets/css/app/typo.scss b/assets/css/app/typo.scss index f30e27b..7873701 100644 --- a/assets/css/app/typo.scss +++ b/assets/css/app/typo.scss @@ -16,34 +16,35 @@ // } // .h1, h1 { - font-size: 3.2rem; - line-height: 1.32 + font-size: 3.2rem; + line-height: 1.32; } .h2, h2 { - font-size: 2.6rem; - line-height: 1.35 + font-size: 2.6rem; + line-height: 1.35; } .h3, h3 { - font-size: 2.0rem; - line-height: 1.45 + font-size: 2.0rem; + line-height: 1.45; } .h4, h4 { - font-size: 1.4rem; - line-height: 1.6 + font-size: 1.4rem; + line-height: 1.6; } .h5, h5 { - font-size: 1.1rem; - line-height: 1.75 + font-size: 1.1rem; + line-height: 1.75; } .h6, h6 { - font-size: 1.0rem; - line-height: 1.9 + font-size: 1.0rem; + line-height: 1.9; } + // // p { // margin-top: 17px; @@ -56,7 +57,6 @@ // } // } - .h1, .h2, .h3, @@ -69,8 +69,7 @@ h3, h4, h5, h6 { - font-weight: 600; - margin-bottom: 1rem; - margin-top: 0 + font-weight: 600; + margin-bottom: 1rem; + margin-top: 0; } - diff --git a/assets/css/viewer.scss b/assets/css/viewer.scss index 39b600b..87c8b23 100644 --- a/assets/css/viewer.scss +++ b/assets/css/viewer.scss @@ -1,10 +1,10 @@ body { - margin: 0; - padding: 0; - overflow: hidden; + margin: 0; + padding: 0; + overflow: hidden; } #mesh-viewer { - width: 100vw; - height: 100vh; + width: 100vw; + height: 100vh; } diff --git a/assets/js/admin.js b/assets/js/admin.js index 9e22dfb..0b31647 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -1,6 +1,6 @@ import '../../vendor/murph/murph-core/src/core/Resources/assets/js/admin.js' -require('./admin_modules/simplemde')() +require('./admin_modules/md-editor')() const $ = require('jquery') const Sortable = require('sortablejs').Sortable diff --git a/assets/js/admin_modules/md-editor.js b/assets/js/admin_modules/md-editor.js new file mode 100644 index 0000000..7e20b97 --- /dev/null +++ b/assets/js/admin_modules/md-editor.js @@ -0,0 +1,34 @@ +const Vue = require('vue').default +const VueMarkdownEditor = require('@kangc/v-md-editor') +const githubTheme = require('@kangc/v-md-editor/lib/theme/github.js') +const fr = require('@kangc/v-md-editor/lib/lang/fr-FR').default +const hljs = require('highlight.js') + +VueMarkdownEditor.use(githubTheme, {Hljs: hljs}) +VueMarkdownEditor.lang.use('fr-FR', fr) +Vue.use(VueMarkdownEditor) + +module.exports = () => { + const components = document.querySelectorAll('.markdown-editor') + + components.forEach((component) => { + return new Vue({ + el: component, + template: ` +
+ + +
+ `, + data() { + return { + name: component.getAttribute('data-name'), + value: JSON.parse(component.getAttribute('data-value')), + } + }, + components: { + VueMarkdownEditor + } + }) + }) +} diff --git a/assets/js/admin_modules/simplemde.js b/assets/js/admin_modules/simplemde.js index a198c16..b52b3f3 100644 --- a/assets/js/admin_modules/simplemde.js +++ b/assets/js/admin_modules/simplemde.js @@ -13,8 +13,7 @@ module.exports = () => { autoDownloadFontAwesome: false, spellChecker: false, styleSelectedText: false, - toolbar: [ - { + toolbar: [{ name: 'bold', action: Editor.toggleBold, className: 'fa fa-bold', diff --git a/assets/js/app.js b/assets/js/app.js index 2b676d9..d83ce4a 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -9,7 +9,7 @@ const Code = require('./app/code') const Knmc = require('./app/knmc') const VideoRatio = require('./app/video-ratio') const Stats = require('./app/stats') -const Particles = require('./app/particles') +// const Particles = require('./app/particles') const MeshViewer = require('./app/mesh-viewer') const SmallMenu = require('./app/small-menu') @@ -22,11 +22,11 @@ const app = new App([ new Knmc(window), new VideoRatio(window), // new Stats(), - new Particles(window), + // new Particles(window), new MeshViewer(window), new SmallMenu(window) ]) -window.addEventListener('load', function () { +window.addEventListener('load', function() { app.init() }, false) diff --git a/assets/js/app/app.js b/assets/js/app/app.js index d557d9b..8d27254 100644 --- a/assets/js/app/app.js +++ b/assets/js/app/app.js @@ -1,19 +1,19 @@ class App { - constructor(components) { - this.components = components || [] - } + constructor (components) { + this.components = components || [] + } - add(c) { - this.components.push(c) + add (c) { + this.components.push(c) - return this - } + return this + } - init() { - for (let u = 0, x = this.components.length; u < x; u++) { - this.components[u].init() - } + init () { + for (let u = 0, x = this.components.length; u < x; u++) { + this.components[u].init() } + } } module.exports = App diff --git a/assets/js/app/code.js b/assets/js/app/code.js index a2a0d3b..32cec25 100644 --- a/assets/js/app/code.js +++ b/assets/js/app/code.js @@ -22,78 +22,79 @@ require('prismjs/components/prism-shell-session') require('prismjs/plugins/keep-markup/prism-keep-markup') require('prismjs/plugins/line-highlight/prism-line-highlight') require('prismjs/plugins/line-numbers/prism-line-numbers') +require('mermaid') class Code { - constructor(w) { - this.window = w - } + constructor(w) { + this.window = w + } - init() { - Prism.highlightAllUnder(document) - let elements = this.window.document.querySelectorAll('code[data-title], div[data-title]') + init() { + Prism.highlightAllUnder(document) + let elements = this.window.document.querySelectorAll('code[data-title], div[data-title]') - for (let i = 0, len = elements.length; i < len; i++) { - const element = elements[i] + for (let i = 0, len = elements.length; i < len; i++) { + const element = elements[i] - if (element.tagName === 'CODE') { - var code = element - var pre = code.parentNode - var post = pre.parentNode - } else { - var code = element.querySelector('code') + if (element.tagName === 'CODE') { + var code = element + var pre = code.parentNode + var post = pre.parentNode + } else { + var code = element.querySelector('code') - if (!code) { - continue - } - - var pre = code.parentNode - var post = pre.parentNode - } - - if (!pre || !post) { - continue - } - - pre.classList.add('with-title') - - const title = this.window.document.createElement('div') - title.classList.add('code-title') - title.textContent = element.getAttribute('data-title') - - post.insertBefore(title, pre) + if (!code) { + continue } - elements = this.window.document.querySelectorAll('code.window') + var pre = code.parentNode + var post = pre.parentNode + } - for (let i = 0, len = elements.length; i < len; i++) { - const element = elements[i] + if (!pre || !post) { + continue + } - if (element.tagName === 'CODE') { - var code = element - var pre = code.parentNode - var post = pre.parentNode - } else { - var code = element.querySelector('code') + pre.classList.add('with-title') - if (!code) { - continue - } + const title = this.window.document.createElement('div') + title.classList.add('code-title') + title.textContent = element.getAttribute('data-title') - var pre = code.parentNode - var post = pre.parentNode - } - - if (!pre || !post) { - continue - } - - pre.classList.add('with-title') - const win = this.window.document.createElement('div') - win.classList.add('code-window') - - post.insertBefore(win, pre) - } + post.insertBefore(title, pre) } + + elements = this.window.document.querySelectorAll('code.window') + + for (let i = 0, len = elements.length; i < len; i++) { + const element = elements[i] + + if (element.tagName === 'CODE') { + var code = element + var pre = code.parentNode + var post = pre.parentNode + } else { + var code = element.querySelector('code') + + if (!code) { + continue + } + + var pre = code.parentNode + var post = pre.parentNode + } + + if (!pre || !post) { + continue + } + + pre.classList.add('with-title') + const win = this.window.document.createElement('div') + win.classList.add('code-window') + + post.insertBefore(win, pre) + } + } } module.exports = Code diff --git a/assets/js/app/form-pwn.js b/assets/js/app/form-pwn.js index 68ea5e9..13c301f 100644 --- a/assets/js/app/form-pwn.js +++ b/assets/js/app/form-pwn.js @@ -1,32 +1,32 @@ const Routing = require('./routing') class FormPnw { - constructor(w) { - this.window = w - } + constructor (w) { + this.window = w + } - init() { - const doc = this.window.document + init () { + const doc = this.window.document - doc.addEventListener('mousemove', function() { - const forms = doc.querySelectorAll('form[data-form-bot]') + doc.addEventListener('mousemove', function () { + const forms = doc.querySelectorAll('form[data-form-bot]') - for (let i = 0, len = forms.length; i < len; i++) { - const form = forms[i] - let action = form.getAttribute('action') - action = action.replace( - Routing.generate('blog_tech_form_without_javascript', { - _domain: window.location.hostname - }, false) + '?page=', '' - ) + for (let i = 0, len = forms.length; i < len; i++) { + const form = forms[i] + let action = form.getAttribute('action') + action = action.replace( + Routing.generate('blog_tech_form_without_javascript', { + _domain: window.location.hostname + }, false) + '?page=', '' + ) - action = decodeURIComponent(action) + action = decodeURIComponent(action) - form.setAttribute('action', action) - form.removeAttribute('data-form-bot') - } - }) - } + form.setAttribute('action', action) + form.removeAttribute('data-form-bot') + } + }) + } } module.exports = FormPnw diff --git a/assets/js/app/knmc.js b/assets/js/app/knmc.js index 92baeae..703b1f6 100644 --- a/assets/js/app/knmc.js +++ b/assets/js/app/knmc.js @@ -1,36 +1,36 @@ const Mario = require('../../images/mario.gif') class Knmc { - constructor(w) { - this.window = w - } + constructor (w) { + this.window = w + } - init() { - let chars = '' - const seq = '38384040373937396665' - const body = this.window.document.querySelector('body') + init () { + let chars = '' + const seq = '38384040373937396665' + const body = this.window.document.querySelector('body') - body.addEventListener('keyup', function(e) { - chars += e.keyCode.toString() + body.addEventListener('keyup', function (e) { + chars += e.keyCode.toString() - if (chars.indexOf(seq) !== -1) { - chars = '' - const url = Mario - const image = new Image() - image.classList.add('fixed') - image.style.position = 'fixed' - image.style.bottom = '-11px' - image.style.left = '-256px' + if (chars.indexOf(seq) !== -1) { + chars = '' + const url = Mario + const image = new Image() + image.classList.add('fixed') + image.style.position = 'fixed' + image.style.bottom = '-11px' + image.style.left = '-256px' - image.onload = function() { - image.classList.add('knmc') - body.appendChild(image) - } + image.onload = function () { + image.classList.add('knmc') + body.appendChild(image) + } - image.src = url - } - }) - } + image.src = url + } + }) + } } module.exports = Knmc diff --git a/assets/js/app/lazy-load.js b/assets/js/app/lazy-load.js index b9282aa..ba9a32e 100644 --- a/assets/js/app/lazy-load.js +++ b/assets/js/app/lazy-load.js @@ -1,10 +1,10 @@ const lozad = require('lozad') class LazyLoad { - init() { - const observer = lozad('.lazy-img') - observer.observe() - } + init () { + const observer = lozad('.lazy-img') + observer.observe() + } } module.exports = LazyLoad diff --git a/assets/js/app/mesh-viewer.js b/assets/js/app/mesh-viewer.js index 1ffe3d7..4c20636 100644 --- a/assets/js/app/mesh-viewer.js +++ b/assets/js/app/mesh-viewer.js @@ -1,36 +1,36 @@ const tingle = require('tingle.js/src/tingle.js') class MeshViewer { - constructor(w) { - this.window = w - } + constructor(w) { + this.window = w + } - init() { - const openers = this.window.document.querySelectorAll('*[data-modal]') + init() { + const openers = this.window.document.querySelectorAll('*[data-modal]') - for (let i = 0, len = openers.length; i < len; i++) { - openers[i].addEventListener('click', (e) => { - e.preventDefault() + for (let i = 0, len = openers.length; i < len; i++) { + openers[i].addEventListener('click', (e) => { + e.preventDefault() - let target = e.target + let target = e.target - if (target.tagName != 'A') { - target = target.parentNode - } - - const modal = new tingle.modal({ - footer: false, - stickyFooter: false, - closeMethods: ['overlay', 'button', 'escape'], - closeLabel: 'Close', - cssClass: ['tingle-modal-box--mesh'] - }) - - modal.setContent('') - modal.open() - }) + if (target.tagName != 'A') { + target = target.parentNode } + + const modal = new tingle.modal({ + footer: false, + stickyFooter: false, + closeMethods: ['overlay', 'button', 'escape'], + closeLabel: 'Close', + cssClass: ['tingle-modal-box--mesh'] + }) + + modal.setContent('') + modal.open() + }) } + } } module.exports = MeshViewer diff --git a/assets/js/app/particles.js b/assets/js/app/particles.js index 62fdc70..c9c2f6b 100644 --- a/assets/js/app/particles.js +++ b/assets/js/app/particles.js @@ -1,47 +1,47 @@ require('particles.js') class Particles { - constructor(w) { - this.window = w + constructor(w) { + this.window = w + } + + start() { + if (this.window.innerWidth < 708) { + return } - start() { - if (this.window.innerWidth < 708) { - return - } + const height = this.header.offsetHeight + const width = this.header.offsetWidth - const height = this.header.offsetHeight - const width = this.header.offsetWidth + this.particles.style.maxHeight = height + 'px' + this.particles.style.maxWidth = width + 'px' - this.particles.style.maxHeight = height + 'px' - this.particles.style.maxWidth = width + 'px' + particlesJS.load('particles', '/js/particles.json?v=3') + } - particlesJS.load('particles', '/js/particles.json?v=3') + clean() { + this.particles.innerHTML = '' + } + + init() { + this.particles = this.window.document.getElementById('particles') + + if (!this.particles) { + return } - clean() { - this.particles.innerHTML = '' - } + this.header = this.particles.parentNode - init() { - this.particles = this.window.document.getElementById('particles') + this.clean() + this.start() - if (!this.particles) { - return - } + const that = this - this.header = this.particles.parentNode - - this.clean() - this.start() - - const that = this - - this.window.addEventListener('resize', function() { - that.clean() - that.start() - }, false) - } + this.window.addEventListener('resize', function() { + that.clean() + that.start() + }, false) + } } module.exports = Particles diff --git a/assets/js/app/post.js b/assets/js/app/post.js index 84eb127..e5133f8 100644 --- a/assets/js/app/post.js +++ b/assets/js/app/post.js @@ -1,122 +1,122 @@ const Routing = require('./routing') class Post { - constructor(w) { - this.window = w + constructor (w) { + this.window = w + } + + commentsEvents () { + const document = this.window.document + + const parentCommentIdField = document.getElementById('user_comment_parentCommentId') + + if (!parentCommentIdField) { + return } - commentsEvents() { - const document = this.window.document + const isAnswerAlert = document.getElementById('answer-alert') + const cancelAnswerButton = document.getElementById('cancel-answer') - const parentCommentIdField = document.getElementById('user_comment_parentCommentId') + const toogleAnswerAlert = function () { + if (parentCommentIdField.value) { + isAnswerAlert.classList.remove('hidden') + } else { + isAnswerAlert.classList.add('hidden') + } + } - if (!parentCommentIdField) { - return - } + toogleAnswerAlert() - const isAnswerAlert = document.getElementById('answer-alert') - const cancelAnswerButton = document.getElementById('cancel-answer') - - const toogleAnswerAlert = function() { - if (parentCommentIdField.value) { - isAnswerAlert.classList.remove('hidden') - } else { - isAnswerAlert.classList.add('hidden') - } - } + const answerButtons = document.querySelectorAll('a[data-answer]') + for (let i = 0, len = answerButtons.length; i < len; i++) { + answerButtons[i].addEventListener('click', function (e) { + parentCommentIdField.value = e.target.getAttribute('data-answer') toogleAnswerAlert() + }, false) + } - const answerButtons = document.querySelectorAll('a[data-answer]') + cancelAnswerButton.addEventListener('click', function (e) { + e.preventDefault() - for (let i = 0, len = answerButtons.length; i < len; i++) { - answerButtons[i].addEventListener('click', function(e) { - parentCommentIdField.value = e.target.getAttribute('data-answer') - toogleAnswerAlert() - }, false) + parentCommentIdField.value = null + toogleAnswerAlert() + }, false) + + const previewButton = document.querySelector('.preview-button') + const previewRender = document.querySelector('#preview') + + previewButton.addEventListener('click', function () { + if (previewRender.innerHTML === '') { + previewRender.innerHTML = '

Chargement en cours…

' + } + + const content = document.querySelector('#user_comment_content').value + const httpRequest = new XMLHttpRequest() + + httpRequest.onreadystatechange = function (data) { + if (httpRequest.readyState === 4 && httpRequest.status === 200) { + const json = JSON.parse(httpRequest.response) + previewRender.innerHTML = json.render + document.location.href = '#preview' } + } - cancelAnswerButton.addEventListener('click', function(e) { - e.preventDefault() + httpRequest.open('POST', Routing.generate('api_blog_comment_preview')) + httpRequest.setRequestHeader( + 'Content-Type', + 'application/x-www-form-urlencoded' + ) + httpRequest.send('content=' + encodeURIComponent(content)) + }, false) + } - parentCommentIdField.value = null - toogleAnswerAlert() - }, false) - - const previewButton = document.querySelector('.preview-button') - const previewRender = document.querySelector('#preview') - - previewButton.addEventListener('click', function() { - if (previewRender.innerHTML === '') { - previewRender.innerHTML = '

Chargement en cours…

' - } - - const content = document.querySelector('#user_comment_content').value - const httpRequest = new XMLHttpRequest() - - httpRequest.onreadystatechange = function(data) { - if (httpRequest.readyState === 4 && httpRequest.status === 200) { - const json = JSON.parse(httpRequest.response) - previewRender.innerHTML = json.render - document.location.href = '#preview' - } - } - - httpRequest.open('POST', Routing.generate('api_blog_comment_preview')) - httpRequest.setRequestHeader( - 'Content-Type', - 'application/x-www-form-urlencoded' - ) - httpRequest.send('content=' + encodeURIComponent(content)) + imagesEvents () { + const document = this.window.document + let isFullscreen = false + const images = document.querySelectorAll('.body img') + + const handleClick = function (image) { + if (isFullscreen) { + if (document.exitFullscreen) { + document.exitFullscreen() + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen() + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen() + } + } else { + if (image.requestFullscreen) { + image.requestFullscreen() + } else if (image.webkitRequestFullscreen) { + image.webkitRequestFullscreen() + } else if (image.mozRequestFullScreen) { + image.mozRequestFullScreen() + } + } + + isFullscreen = !isFullscreen + } + + for (let i = 0, len = images.length; i < len; i++) { + const image = images[i] + + if (image.parentNode.tagName === 'A') { + continue + } + + (function (i) { + i.addEventListener('click', function () { + handleClick(i) }, false) + })(image) } + } - imagesEvents() { - const document = this.window.document - let isFullscreen = false - const images = document.querySelectorAll('.body img') - - const handleClick = function(image) { - if (isFullscreen) { - if (document.exitFullscreen) { - document.exitFullscreen() - } else if (document.webkitExitFullscreen) { - document.webkitExitFullscreen() - } else if (document.mozCancelFullScreen) { - document.mozCancelFullScreen() - } - } else { - if (image.requestFullscreen) { - image.requestFullscreen() - } else if (image.webkitRequestFullscreen) { - image.webkitRequestFullscreen() - } else if (image.mozRequestFullScreen) { - image.mozRequestFullScreen() - } - } - - isFullscreen = !isFullscreen - } - - for (let i = 0, len = images.length; i < len; i++) { - const image = images[i] - - if (image.parentNode.tagName === 'A') { - continue - } - - (function(i) { - i.addEventListener('click', function() { - handleClick(i) - }, false) - })(image) - } - } - - init() { - this.commentsEvents() - this.imagesEvents() - } + init () { + this.commentsEvents() + this.imagesEvents() + } } module.exports = Post diff --git a/assets/js/app/px-image.js b/assets/js/app/px-image.js index fa77d1b..da59baf 100644 --- a/assets/js/app/px-image.js +++ b/assets/js/app/px-image.js @@ -1,34 +1,34 @@ class PxImage { - constructor(w) { - this.window = w - } + constructor (w) { + this.window = w + } - init() { - const doc = this.window.document + init () { + const doc = this.window.document - const images = doc.querySelectorAll('.quick-image img, .card figure img') + const images = doc.querySelectorAll('.quick-image img, .card figure img') - for (let i = 0, len = images.length; i < len; i++) { - ((image) => { - const source = image.getAttribute('data-src') - const sourceError = image.getAttribute('data-src-error') - const color = image.getAttribute('data-color') - const loader = new Image() + for (let i = 0, len = images.length; i < len; i++) { + ((image) => { + const source = image.getAttribute('data-src') + const sourceError = image.getAttribute('data-src-error') + const color = image.getAttribute('data-color') + const loader = new Image() - loader.onload = () => { - image.style.background = `${color ? color : null} url(${source})` - image.style.backgroundSize = 'cover' - image.style.backgroundPosition = 'center' - } - - loader.onerror = () => { - image.style.background = `${color ? color : null} url('${sourceError}') center center` - } - - loader.src = source - })(images[i]) + loader.onload = () => { + image.style.background = `${color || null} url(${source})` + image.style.backgroundSize = 'cover' + image.style.backgroundPosition = 'center' } + + loader.onerror = () => { + image.style.background = `${color || null} url('${sourceError}') center center` + } + + loader.src = source + })(images[i]) } + } } module.exports = PxImage diff --git a/assets/js/app/small-menu.js b/assets/js/app/small-menu.js index 7629fc9..0f06f81 100644 --- a/assets/js/app/small-menu.js +++ b/assets/js/app/small-menu.js @@ -1,24 +1,24 @@ const Routing = require('./routing') class SmallMenu { - constructor(w) { - this.window = w - } + constructor(w) { + this.window = w + } - addEvent() { - const document = this.window.document - const menu = document.querySelector('.small-menu') - const opener = document.querySelector('.menu-opener') + addEvent() { + const document = this.window.document + const menu = document.querySelector('.small-menu') + const opener = document.querySelector('.menu-opener') - opener.addEventListener('click', () => { - menu.classList.toggle('is-open') - opener.classList.toggle('is-open') - }) - } + opener.addEventListener('click', () => { + menu.classList.toggle('is-open') + opener.classList.toggle('is-open') + }) + } - init() { - this.addEvent() - } + init() { + this.addEvent() + } } module.exports = SmallMenu diff --git a/assets/js/app/stats.js b/assets/js/app/stats.js index 969018d..9cf8f2a 100644 --- a/assets/js/app/stats.js +++ b/assets/js/app/stats.js @@ -1,19 +1,19 @@ class Stats { - init() { - (function(f, a, t, h, o, m) { - a[h] = a[h] || function() { - (a[h].q = a[h].q || []).push(arguments) - } - o = f.createElement('script'), - m = f.getElementsByTagName('script')[0] - o.async = 1; - o.src = t; - o.id = 'fathom-script' - m.parentNode.insertBefore(o, m) - })(document, window, '//ftm.deblan.org/tracker.js', 'fathom') - fathom('set', 'siteId', 'HQAWS') - fathom('trackPageview') - } + init() { + (function(f, a, t, h, o, m) { + a[h] = a[h] || function() { + (a[h].q = a[h].q || []).push(arguments) + } + o = f.createElement('script'), + m = f.getElementsByTagName('script')[0] + o.async = 1; + o.src = t; + o.id = 'fathom-script' + m.parentNode.insertBefore(o, m) + })(document, window, '//ftm.deblan.org/tracker.js', 'fathom') + fathom('set', 'siteId', 'HQAWS') + fathom('trackPageview') + } } module.exports = Stats diff --git a/assets/js/app/video-ratio.js b/assets/js/app/video-ratio.js index 27da274..96aff4d 100644 --- a/assets/js/app/video-ratio.js +++ b/assets/js/app/video-ratio.js @@ -1,15 +1,15 @@ class VideoRatio { - constructor(w) { - this.window = w - } + constructor (w) { + this.window = w + } - init() { - const videos = this.window.document.querySelectorAll('.video-ratio') + init () { + const videos = this.window.document.querySelectorAll('.video-ratio') - for (let i = 0, len = videos.length; i < len; i++) { - videos[i].style.paddingBottom = videos[i].getAttribute('data-ratio') - } + for (let i = 0, len = videos.length; i < len; i++) { + videos[i].style.paddingBottom = videos[i].getAttribute('data-ratio') } + } } module.exports = VideoRatio diff --git a/assets/js/viewer.js b/assets/js/viewer.js index ca2a888..2742df1 100644 --- a/assets/js/viewer.js +++ b/assets/js/viewer.js @@ -2,10 +2,11 @@ import '../css/viewer.scss' const container = document.getElementById('mesh-viewer') const viewer = new StlViewer( - container, - { + container, { auto_rotate: true, allow_drag_and_drop: true, - models: [{ filename: container.getAttribute('data-file') }] + models: [{ + filename: container.getAttribute('data-file') + }] } ) diff --git a/assets/webapp/favicon.svg b/assets/webapp/favicon.svg new file mode 100644 index 0000000..d31734c --- /dev/null +++ b/assets/webapp/favicon.svg @@ -0,0 +1,85 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/bin/messenger b/bin/messenger new file mode 100755 index 0000000..525afc9 --- /dev/null +++ b/bin/messenger @@ -0,0 +1,129 @@ +#!/bin/sh + +set -eu + +usage() { + printf "Usage: %s [-l DEBUG_LEVEL] [-h] start|stop|restart\n" "$0" +} + +help() { + cat << EOH + SYNOPSIS + $0 [-l DEBUG_LEVEL] [-h] -a start|stop|restart + + DESCRIPTION + + $0 manages symfony messenger + + OPTIONS + + -h Show this help + + -l debug|info|notice|warning|error + Debug level + + -a start|stop|restart|status +EOH +} + +on_interrupt() { + log -l notice "" + log -l notice "Process aborted!" + + exit 130 +} + +start_messenger() { + nohup php8.1 bin/console messenger:consume 2>/dev/null >/dev/null & + log -t -l notice "Started" +} + +stop_messenger() { + php8.1 bin/console messenger:stop-workers 2>/dev/null >/dev/null + log -t -l notice "Stopped" +} + +get_pid() { + pgrep -f messenger:consume +} + +main() { + cd "$(dirname "0")" + + ACTION= + + while getopts "l:ha:" option; do + case "${option}" in + h) help; exit 0;; + l) LOG_VERBOSE="$OPTARG";; + a) ACTION="$OPTARG";; + ?) log -l error "$(usage)"; exit 1;; + esac + done + + if [ "$ACTION" = "start" ]; then + start_messenger + elif [ "$ACTION" = "stop" ]; then + stop_messenger + elif [ "$ACTION" = "restart" ]; then + stop_messenger + start_messenger + elif [ "$ACTION" = "status" ]; then + get_pid + else + log -l error "Action is required." + fi + + exit 0 +} + +log() { + LOG_VERBOSE="${LOG_VERBOSE:-info}" + LEVEL=info + TIME= + + while getopts "tl:" option; do + case "${option}" in + l) LEVEL="$OPTARG"; shift $((OPTIND-1));; + t) TIME="$(printf "[%s] " "$(date +'%Y-%m-%dT%H:%M:%S.%s')")"; shift $((OPTIND-1));; + *) exit 1;; + esac + done + + if [ -t 2 ] && [ -z "${NO_COLOR-}" ]; then + case "${LEVEL}" in + debug) COLOR="$(tput setaf 3)";; + notice) COLOR="$(tput setaf 4)";; + warning) COLOR="$(tput setaf 5)";; + error) COLOR="$(tput setaf 1)";; + *) COLOR="$(tput sgr0)";; + esac + fi + + case "${LEVEL}" in + debug) LEVEL=100;; + notice) LEVEL=250;; + warning) LEVEL=300;; + error) LEVEL=400;; + *) LEVEL=200;; + esac + + case "${LOG_VERBOSE}" in + debug) LOG_VERBOSE_VALUE=100;; + notice) LOG_VERBOSE_VALUE=250;; + warning) LOG_VERBOSE_VALUE=300;; + error) LOG_VERBOSE_VALUE=400;; + *) LOG_VERBOSE_VALUE=200;; + esac + + if [ $LEVEL -ge $LOG_VERBOSE_VALUE ]; then + printf "%s\n" "$*" | while IFS='' read -r LINE; do + printf "%s%s%s\n" "${COLOR:-}" "${TIME:-}" "$LINE" >&2 + done + fi +} + +trap on_interrupt INT + +main "$@" + diff --git a/composer.json b/composer.json index c4c73a4..9f8b4a7 100644 --- a/composer.json +++ b/composer.json @@ -8,9 +8,12 @@ "beberlei/doctrineextensions": "^1.3", "friendsofsymfony/jsrouting-bundle": "^2.7", "gregwar/captcha-bundle": "^2.2", + "guzzlehttp/guzzle": "^7.8", + "influxdata/influxdb-client-php": "^3.4", "knplabs/knp-markdown-bundle": "^1.9", "knplabs/knp-menu-bundle": "^3.1", - "murph/murph-core": "^1.18", + "murph/murph-core": "dev-master", + "symfony/messenger": "5.4.*", "twig/intl-extra": "^3.5" }, "require-dev": { @@ -31,7 +34,8 @@ "sort-packages": true, "allow-plugins": { "symfony/flex": true, - "symfony/runtime": true + "symfony/runtime": true, + "php-http/discovery": true } }, "autoload": { diff --git a/config/packages/app.yaml b/config/packages/app.yaml index 8b0d1ad..7287936 100644 --- a/config/packages/app.yaml +++ b/config/packages/app.yaml @@ -1,7 +1,7 @@ core: site: name: "Blog" - logo: "build/images/core/logo.svg" + logo: "build/webapp/favicon.svg" controllers: - {name: 'LinkController:links', action: 'App\Controller\LinkController:links'} - {name: 'ContactController::contact', action: 'App\Controller\ContactController::contact'} @@ -81,6 +81,7 @@ core: - image/jpg - image/jpeg - image/gif + - image/webp - image/svg+xml - video/mp4 - audio/mpeg3 diff --git a/config/packages/liip_imagine.yaml b/config/packages/liip_imagine.yaml index e3cd592..157b0a7 100644 --- a/config/packages/liip_imagine.yaml +++ b/config/packages/liip_imagine.yaml @@ -13,18 +13,21 @@ liip_imagine: max: [600, 600] crop: size: [600, 270] + start: [0, 0] project_preview_filter: filters: downscale: max: [600, 600] crop: size: [600, 270] + start: [0, 0] post_preview_filter: filters: downscale: max: [600, 600] crop: size: [600, 300] + start: [0, 0] site_avatar: filters: downscale: diff --git a/config/packages/messenger.yaml b/config/packages/messenger.yaml new file mode 100644 index 0000000..ce96395 --- /dev/null +++ b/config/packages/messenger.yaml @@ -0,0 +1,7 @@ +framework: + messenger: + transports: + async: "%env(MESSENGER_TRANSPORT_DSN)%" + + routing: + 'App\Message\PageViewMessage': async diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index e402952..9e21266 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -1,6 +1,6 @@ twig: default_path: '%kernel.project_dir%/templates' - form_themes: ['@Core/form/bootstrap_4_form_theme.html.twig'] + form_themes: ['form/bootstrap_4_form_theme.html.twig'] auto_reload: true paths: '%kernel.project_dir%/templates/core/': Core diff --git a/config/services.yaml b/config/services.yaml index 832f63b..2fde6d0 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -4,6 +4,11 @@ # Put parameters here that don't need to change on each machine where the app is deployed # https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration parameters: + influxdb_url: '%env(INFLUXDB_URL)%' + influxdb_token: '%env(INFLUXDB_TOKEN)%' + influxdb_bucket: '%env(INFLUXDB_BUCKET)%' + influxdb_org: '%env(INFLUXDB_ORG)%' + influxdb_debug: '%env(INFLUXDB_DEBUG)%' services: # default configuration for services in *this* file @@ -47,6 +52,14 @@ services: resource: '../src/Controller/' tags: ['controller.service_arguments'] + App\Api\InfluxDB: + arguments: + $url: '%influxdb_url%' + $token: '%influxdb_token%' + $bucket: '%influxdb_bucket%' + $org: '%influxdb_org%' + $debug: '%influxdb_debug%' + site.route_loader: class: App\Core\Router\SiteRouteLoader tags: [routing.loader] @@ -69,5 +82,9 @@ services: tags: - {name: markdown.parser, alias: comment} + App\EventListener\StatListener: + tags: + - { name: kernel.event_listener, event: kernel.request } + # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones diff --git a/package.json b/package.json index cd6f247..4eb7cdd 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,16 @@ "build": "./node_modules/.bin/encore production" }, "dependencies": { + "@kangc/v-md-editor": "^1.7.12", "daisyui": "^2.31.0", + "editorjs-hyperlink": "^1.0.6", + "editorjs-inline-tool": "^0.4.0", "encore": "^0.0.30-beta", "lozad": "^1.16.0", - "murph-project": "^1", + "mermaid": "^11.0.2", + "murph-project": "^1.9.4", "particles.js": "^2.0.0", "prismjs": "^1.23.0", - "simplemde": "^1.11.2", "tingle.js": "^0.16.0", "vanillajs-datepicker": "^1.1.4", "vue": "^2.6.14" @@ -25,5 +28,6 @@ "postcss": "^8.4.16", "postcss-loader": "^7.0.1", "tailwindcss": "^3.1.8" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/Analytic/DateRangeAnalytic.php b/src/Analytic/DateRangeAnalytic.php index bf5bb89..7e34011 100644 --- a/src/Analytic/DateRangeAnalytic.php +++ b/src/Analytic/DateRangeAnalytic.php @@ -29,7 +29,7 @@ class DateRangeAnalytic extends BaseDateRangeAnalytic foreach ($entities as $key => $entity) { if ('view' === $type) { - if ($this->path === null || str_starts_with($entity->getPath(), $this->path)) { + if (null === $this->path || str_starts_with($entity->getPath(), $this->path)) { $newEntities[] = $entity; } } diff --git a/src/Api/InfluxDB.php b/src/Api/InfluxDB.php new file mode 100644 index 0000000..afe854f --- /dev/null +++ b/src/Api/InfluxDB.php @@ -0,0 +1,46 @@ + + */ +class InfluxDB +{ + protected ?Client $client = null; + + public function __construct( + protected ?string $url, + protected ?string $token, + protected ?string $bucket, + protected ?string $org, + protected bool $debug = false + ) { + if (isset($this->url, $this->token, $this->bucket, $this->org)) { + $this->client = new Client([ + 'url' => $this->url, + 'token' => $this->token, + 'bucket' => $this->bucket, + 'org' => $this->org, + 'debug' => $this->debug, + 'precision' => WritePrecision::S, + 'timeout' => 1, + ]); + } + } + + public function isAvailable(): bool + { + return null !== $this->getClient(); + } + + public function getClient(): ?Client + { + return $this->client; + } +} diff --git a/src/Api/TTRssClient.php b/src/Api/TTRssClient.php index 7c538cc..c5cad19 100644 --- a/src/Api/TTRssClient.php +++ b/src/Api/TTRssClient.php @@ -14,7 +14,7 @@ class TTRssClient $result = @file_get_contents('https://tiny.deblan.org/deblan_api/?itemsPerPage=10&page='.$page); if ($result) { - $result = str_replace('\\u0092', "'", $result); + $result = str_replace('\u0092', "'", $result); $result = str_replace(''', "'", $result); return json_decode($result, true); diff --git a/src/Controller/Blog/CategoryAdminController.php b/src/Controller/Blog/CategoryAdminController.php index 008ccba..96e52f5 100644 --- a/src/Controller/Blog/CategoryAdminController.php +++ b/src/Controller/Blog/CategoryAdminController.php @@ -43,6 +43,7 @@ class CategoryAdminController extends CrudController ->setMaxPerPage('index', 100) ->setView('form', 'blog/category_admin/_form.html.twig') + ->setDoubleClick('index', true) ->setDefaultSort('index', 'title', 'asc') diff --git a/src/Controller/Blog/PostAdminController.php b/src/Controller/Blog/PostAdminController.php index 41d7308..df78833 100644 --- a/src/Controller/Blog/PostAdminController.php +++ b/src/Controller/Blog/PostAdminController.php @@ -2,27 +2,30 @@ namespace App\Controller\Blog; +use App\Analytic\DateRangeAnalytic; use App\Core\Controller\Admin\Crud\CrudController; use App\Core\Crud\CrudConfiguration; use App\Core\Crud\Field\DatetimeField; use App\Core\Crud\Field\TextField; +use App\Core\Entity\EntityInterface; use App\Core\Form\FileUploadHandler; use App\Core\Manager\EntityManager; +use App\Core\Repository\Site\NodeRepository; +use App\Entity\Blog\Post; use App\Entity\Blog\Post as Entity; use App\Factory\Blog\PostFactory as EntityFactory; use App\Form\Blog\Filter\PostFilterType; use App\Form\Blog\PostType; -use App\Form\Blog\PostType as EntityType; use App\Repository\Blog\PostRepositoryQuery as RepositoryQuery; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\DateTimeType; +use Symfony\Component\Form\Form; +use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\Routing\Annotation\Route; -use Symfony\Component\Form\Form; -use App\Core\Entity\EntityInterface; -use App\Entity\Blog\Post; -use App\Analytic\DateRangeAnalytic; -use App\Core\Repository\Site\NodeRepository; +use Symfony\Component\Validator\Constraints\NotBlank; #[Route(path: '/admin/blog/post')] class PostAdminController extends CrudController @@ -37,6 +40,7 @@ class PostAdminController extends CrudController ->setPageRoute('index', 'admin_blog_post_index') ->setPageRoute('edit', 'admin_blog_post_edit') + ->setPageRoute('inline_edit', 'admin_blog_post_inline_edit') ->setPageRoute('new', 'admin_blog_post_new') ->setPageRoute('show', 'admin_blog_post_show') ->setPageRoute('delete', 'admin_blog_post_delete') @@ -54,6 +58,7 @@ class PostAdminController extends CrudController ->setView('edit', 'blog/post_admin/edit.html.twig') ->setView('show', 'blog/post_admin/show.html.twig') ->setView('index', 'blog/post_admin/index.html.twig') + ->setDoubleClick('index', true) ->setDefaultSort('index', 'id', 'desc') ->setField('index', 'Titre', TextField::class, [ @@ -62,7 +67,7 @@ class PostAdminController extends CrudController 'attr' => ['class' => 'miw-400'], ]) ->setField('index', 'ID', TextField::class, [ - 'property_builder' => function(EntityInterface $entity) { + 'property_builder' => function (EntityInterface $entity) { return sprintf('#%d', $entity->getId()); }, 'sort' => ['id', '.id'], @@ -79,19 +84,55 @@ class PostAdminController extends CrudController 'format' => 'd/m/Y H:i', 'sort' => ['publishedAt', '.publishedAt'], 'attr' => ['class' => 'miw-200'], + 'inline_form' => function (FormBuilderInterface $builder) { + $builder->add( + 'publishedAt', + DateTimeType::class, + [ + 'label' => 'Date de publication', + 'required' => false, + 'html5' => true, + 'widget' => 'single_text', + 'attr' => [ + 'data-datetime' => '', + ], + 'constraints' => [ + ], + ] + ); + }, ]) ->setField('index', 'Status', TextField::class, [ 'view' => 'blog/post_admin/field/status.html.twig', 'sort' => ['status', '.status'], 'attr' => ['class' => 'miw-100'], + 'inline_form' => function (FormBuilderInterface $builder) { + $builder->add( + 'status', + ChoiceType::class, + [ + 'label' => 'Statut', + 'required' => true, + 'choices' => [ + 'Brouillon' => Entity::DRAFT, + 'Publié' => Entity::PUBLISHED, + ], + 'attr' => [ + ], + 'constraints' => [ + new NotBlank(), + ], + ] + ); + }, ]) - ->setBatchAction('index', 'delete', 'Delete', function(EntityInterface $entity, EntityManager $manager) { + ->setBatchAction('index', 'delete', 'Delete', function (EntityInterface $entity, EntityManager $manager) { $manager->delete($entity); }) - ->setBatchAction('index', 'draft', 'Statut : publier', function(EntityInterface $entity, EntityManager $manager) { + ->setBatchAction('index', 'draft', 'Statut : publier', function (EntityInterface $entity, EntityManager $manager) { $manager->update($entity->setStatus(Post::PUBLISHED)); }) - ->setBatchAction('index', 'publish', 'Statut : brouillon', function(EntityInterface $entity, EntityManager $manager) { + ->setBatchAction('index', 'publish', 'Statut : brouillon', function (EntityInterface $entity, EntityManager $manager) { $manager->update($entity->setStatus(Post::DRAFT)); }) ; @@ -110,7 +151,7 @@ class PostAdminController extends CrudController $factory->create($this->getUser()), $entityManager, $request, - function(Entity $entity, Form $form, Request $request) use ($fileUpload) { + function (Entity $entity, Form $form, Request $request) use ($fileUpload) { $directory = 'uploads/post/'.date('Y'); $fileUpload->handleForm( @@ -131,7 +172,7 @@ class PostAdminController extends CrudController $entity, $entityManager, $request, - function(Entity $entity, Form $form, Request $request) use ($fileUpload) { + function (Entity $entity, Form $form, Request $request) use ($fileUpload) { $directory = 'uploads/post/'.date('Y'); $fileUpload->handleForm( @@ -145,6 +186,12 @@ class PostAdminController extends CrudController ); } + #[Route(path: '/inline_edit/{entity}/{context}/{label}', name: 'admin_blog_post_inline_edit', methods: ['GET', 'POST'])] + public function inlineEdit(string $context, string $label, Entity $entity, EntityManager $entityManager, Request $request): Response + { + return $this->doInlineEdit($context, $label, $entity, $entityManager, $request); + } + #[Route(path: '/show/{entity}', name: 'admin_blog_post_show')] public function show(Entity $entity): Response { @@ -220,8 +267,7 @@ class PostAdminController extends CrudController DateRangeAnalytic $analytic, NodeRepository $nodeRepository, string $range = '7days' - ): Response - { + ): Response { if (!in_array($range, ['7days', '30days', '90days', '1year'])) { throw $this->createNotFoundException(); } diff --git a/src/Controller/Blog/PostController.php b/src/Controller/Blog/PostController.php index fe4e369..73bb564 100644 --- a/src/Controller/Blog/PostController.php +++ b/src/Controller/Blog/PostController.php @@ -7,19 +7,19 @@ use App\Core\Controller\Site\PageController; use App\Core\Manager\EntityManager; use App\Core\Site\SiteRequest; use App\Core\Site\SiteStore; +use App\Core\Twig\Extension\BuilderExtension; +use App\Core\Twig\Extension\EditorJsExtension; use App\Entity\Blog\Category; use App\Entity\Blog\Post; use App\Factory\Blog\CommentFactory; use App\Form\Blog\UserCommentType; +use App\Manager\PostFollowManager; use App\Markdown\Parser\Post as PostParser; use App\Repository\Blog\PostRepositoryQuery; use App\UrlGenerator\PostGenerator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use App\Factory\Blog\PostFollowFactory; -use App\Manager\PostFollowManager; -use App\Core\Twig\Extension\EditorJsExtension; class PostController extends PageController { @@ -92,9 +92,9 @@ class PostController extends PageController ]); } - public function posts(int $page = 1): Response + public function posts(Request $request, int $page = 1): Response { - $entities = $this->createQuery() + $entities = $this->createQuery($request->query->has('preview') && $this->getUser()) ->paginate($page, 9) ; @@ -149,26 +149,34 @@ class PostController extends PageController ]); } - public function tag(string $tag, int $page = 1): Response - { - } + public function tag(string $tag, int $page = 1): Response {} - public function createQuery(): PostRepositoryQuery + public function createQuery(bool $isPreview = false): PostRepositoryQuery { - return $this->postQuery->create() + $query = $this->postQuery->create() ->orderBy('.publishedAt', 'DESC') - ->published() ; + + if (!$isPreview) { + $query->published(); + } + + return $query; } - public function rss(PostParser $parser, EditorJsExtension $editorJsExtension): Response - { + public function rss( + PostParser $parser, + EditorJsExtension $editorJsExtension, + BuilderExtension $builderExtension + ): Response { $entities = $this->createQuery()->paginate(1, 20); $items = []; foreach ($entities as $entity) { - if ($entity->getContentFormat() === 'editorjs') { + if ('editorjs' === $entity->getContentFormat()) { $description = $editorJsExtension->buildHtml($entity->getContent()); + } elseif ('builder' === $entity->getContentFormat()) { + $description = $builderExtension->buildHtml($entity->getContent()); } else { $description = $parser->transformMarkdown($entity->getContent()); } @@ -189,7 +197,7 @@ class PostController extends PageController 'post' => $entity->getId(), 'slug' => $entity->getSlug(), ], UrlGeneratorInterface::ABSOLUTE_URL), - 'linkGemini' => sprintf('gemini://deblan.io/posts/%d.gmi', $entity->getId()), + 'linkGemini' => sprintf('gemini://deblan.fr/posts/%d.gmi', $entity->getId()), ]; } diff --git a/src/Controller/ContactController.php b/src/Controller/ContactController.php index dffd8e3..26703f4 100644 --- a/src/Controller/ContactController.php +++ b/src/Controller/ContactController.php @@ -4,10 +4,10 @@ namespace App\Controller; use App\Core\Controller\Site\PageController; use App\Core\Notification\MailNotifier; +use App\Core\Setting\SettingManager; use App\Form\ContactType; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use App\Core\Setting\SettingManager; class ContactController extends PageController { diff --git a/src/Controller/DashboardAdminController.php b/src/Controller/DashboardAdminController.php index 833cf1a..7fea805 100644 --- a/src/Controller/DashboardAdminController.php +++ b/src/Controller/DashboardAdminController.php @@ -3,6 +3,8 @@ namespace App\Controller; use App\Core\Controller\Dashboard\DashboardAdminController as Controller; +use App\Repository\Blog\PostRepositoryQuery; +use App\Repository\ProjectRepositoryQuery; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; @@ -10,8 +12,23 @@ use Symfony\Component\Routing\Annotation\Route; class DashboardAdminController extends Controller { #[Route(path: '/', name: 'admin_dashboard_index')] - public function index(): Response - { - return $this->render('admin/dashboard.html.twig'); + public function index( + PostRepositoryQuery $postQuery, + ProjectRepositoryQuery $projectQuery + ): Response { + $posts = $postQuery->create() + ->orderBy('.id', 'DESC') + ->paginate(1, 4) + ; + + $projects = $projectQuery->create() + ->orderBy('.id', 'DESC') + ->paginate(1, 3) + ; + + return $this->render('admin/dashboard.html.twig', [ + 'posts' => $posts, + 'projects' => $projects, + ]); } } diff --git a/src/Controller/ProjectAdminController.php b/src/Controller/ProjectAdminController.php index 57bbc6a..83034db 100644 --- a/src/Controller/ProjectAdminController.php +++ b/src/Controller/ProjectAdminController.php @@ -7,6 +7,7 @@ use App\Core\Crud\CrudConfiguration; use App\Core\Crud\Field; use App\Core\Entity\EntityInterface; use App\Core\Manager\EntityManager; +use App\Entity\Project; use App\Entity\Project as Entity; use App\Factory\ProjectFactory as Factory; use App\Form\ProjectType as Type; @@ -15,7 +16,6 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\Routing\Annotation\Route; -use App\Entity\Project; class ProjectAdminController extends CrudController { @@ -114,13 +114,13 @@ class ProjectAdminController extends CrudController 'attr' => ['class' => 'miw-100'], ]) - ->setBatchAction('index', 'delete', 'Delete', function(EntityInterface $entity, EntityManager $manager) { + ->setBatchAction('index', 'delete', 'Delete', function (EntityInterface $entity, EntityManager $manager) { $manager->delete($entity); }) - ->setBatchAction('index', 'draft', 'Statut : publier', function(EntityInterface $entity, EntityManager $manager) { + ->setBatchAction('index', 'draft', 'Statut : publier', function (EntityInterface $entity, EntityManager $manager) { $manager->update($entity->setStatus(Project::PUBLISHED)); }) - ->setBatchAction('index', 'publish', 'Statut : brouillon', function(EntityInterface $entity, EntityManager $manager) { + ->setBatchAction('index', 'publish', 'Statut : brouillon', function (EntityInterface $entity, EntityManager $manager) { $manager->update($entity->setStatus(Project::DRAFT)); }) ; diff --git a/src/Controller/StlMeshAdminController.php b/src/Controller/StlMeshAdminController.php index 6b052f5..f9f9614 100644 --- a/src/Controller/StlMeshAdminController.php +++ b/src/Controller/StlMeshAdminController.php @@ -110,7 +110,7 @@ class StlMeshAdminController extends CrudController 'attr' => ['class' => 'col-md-12'], ]) - ->setBatchAction('index', 'delete', 'Delete', function(EntityInterface $entity, EntityManager $manager) { + ->setBatchAction('index', 'delete', 'Delete', function (EntityInterface $entity, EntityManager $manager) { $manager->delete($entity); }) ; diff --git a/src/Controller/StlMeshController.php b/src/Controller/StlMeshController.php index 594bddf..066552f 100644 --- a/src/Controller/StlMeshController.php +++ b/src/Controller/StlMeshController.php @@ -2,13 +2,11 @@ namespace App\Controller; -use App\Api\TTRssClient; use App\Core\Controller\Site\PageController; -use App\Markdown\Parser\Post as PostParser; -use Symfony\Component\HttpFoundation\Response; -use App\Repository\StlMeshRepositoryQuery; use App\Entity\StlMesh; +use App\Repository\StlMeshRepositoryQuery; use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\Routing\Annotation\Route; @@ -18,7 +16,8 @@ class StlMeshController extends PageController { $pager = $query->create() ->orderBy('.sortOrder') - ->paginate(1, 200); + ->paginate(1, 200) + ; return $this->defaultRender($this->siteRequest->getPage()->getTemplate(), [ 'pager' => $pager, diff --git a/src/Controller/TextController.php b/src/Controller/TextController.php index 39cb0a9..d72aea3 100644 --- a/src/Controller/TextController.php +++ b/src/Controller/TextController.php @@ -2,9 +2,7 @@ namespace App\Controller; -use App\Api\TTRssClient; use App\Core\Controller\Site\PageController; -use App\Markdown\Parser\Post as PostParser; use Symfony\Component\HttpFoundation\Response; class TextController extends PageController diff --git a/src/Controller/UserAdminController.php b/src/Controller/UserAdminController.php new file mode 100644 index 0000000..9970f51 --- /dev/null +++ b/src/Controller/UserAdminController.php @@ -0,0 +1,79 @@ + '\d+'])] + public function index(RepositoryQuery $query, Request $request, Session $session, int $page = 1): Response + { + return parent::index($query, $request, $session, $page); + } + + #[Route(path: '/admin/user/new', name: 'admin_user_new', methods: ['GET', 'POST'])] + public function new(Factory $factory, EntityManager $entityManager, Request $request, TokenGenerator $tokenGenerator): Response + { + return parent::new($factory, $entityManager, $request, $tokenGenerator); + } + + #[Route(path: '/admin/user/show/{entity}', name: 'admin_user_show', methods: ['GET'])] + public function show(Entity $entity): Response + { + return parent::show($entity); + } + + #[Route(path: '/admin/user/filter', name: 'admin_user_filter', methods: ['GET'])] + public function filter(Session $session): Response + { + return parent::filter($session); + } + + #[Route(path: '/admin/user/edit/{entity}', name: 'admin_user_edit', methods: ['GET', 'POST'])] + public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response + { + return parent::edit($entity, $entityManager, $request); + } + + #[Route(path: '/admin/user/inline_edit/{entity}/{context}/{label}', name: 'admin_user_inline_edit', methods: ['GET', 'POST'])] + public function inlineEdit(string $context, string $label, Entity $entity, EntityManager $entityManager, Request $request): Response + { + return parent::inlineEdit($context, $label, $entity, $entityManager, $request); + } + + #[Route(path: '/admin/user/delete/{entity}', name: 'admin_user_delete', methods: ['DELETE', 'POST'])] + public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response + { + return parent::delete($entity, $entityManager, $request); + } + + #[Route(path: '/admin/user/resetting_request/{entity}', name: 'admin_user_resetting_request', methods: ['POST'])] + public function requestResetting(Entity $entity, EventDispatcherInterface $eventDispatcher, Request $request): Response + { + return parent::requestResetting($entity, $eventDispatcher, $request); + } + + protected function getConfiguration(): CrudConfiguration + { + if ($this->configuration) { + return $this->configuration; + } + + return parent::getConfiguration() + ->setView('form', 'admin/user_admin/_form.html.twig') + ->setView('show_entity', 'admin/user_admin/_show.html.twig') + ; + } +} diff --git a/src/DependencyInjection/AppExtension.php b/src/DependencyInjection/AppExtension.php index 664ea17..3678680 100644 --- a/src/DependencyInjection/AppExtension.php +++ b/src/DependencyInjection/AppExtension.php @@ -7,9 +7,6 @@ use Symfony\Component\DependencyInjection\Extension\Extension; class AppExtension extends Extension { - /** - * {@inheritdoc} - */ public function load(array $configs, ContainerBuilder $container) { $configuration = $this->getConfiguration($configs, $container); @@ -18,9 +15,6 @@ class AppExtension extends Extension $container->setParameter('app', $config); } - /** - * {@inheritdoc} - */ public function getConfiguration(array $configs, ContainerBuilder $container) { return new Configuration(); diff --git a/src/Entity/Blog/Comment.php b/src/Entity/Blog/Comment.php index b2b108a..d471924 100644 --- a/src/Entity/Blog/Comment.php +++ b/src/Entity/Blog/Comment.php @@ -55,6 +55,16 @@ class Comment implements EntityInterface $this->postFollows = new ArrayCollection(); } + public function __toString() + { + return sprintf( + '[%s] (%s) %s', + $this->getAuthor(), + $this->getCreatedAt()->format('d/m/Y'), + substr($this->getContent(), 0, 20).'…' + ); + } + public function getId(): ?int { return $this->id; @@ -189,25 +199,12 @@ class Comment implements EntityInterface */ public function getAvatar(): string { - $mail = $this->getEmail() ?? sprintf('%d@deblan.io', $this->getId()); + $mail = $this->getEmail() ?? sprintf('%d@deblan.fr', $this->getId()); $hash = md5($mail); return 'https://cdn.libravatar.org/avatar/'.$hash.'?s=90&d=retro'; } - /** - * {@inheritdoc} - */ - public function __toString() - { - return sprintf( - '[%s] (%s) %s', - $this->getAuthor(), - $this->getCreatedAt()->format('d/m/Y'), - substr($this->getContent(), 0, 20).'…' - ); - } - /** * @return Collection|PostFollow[] */ diff --git a/src/Entity/Blog/Post.php b/src/Entity/Blog/Post.php index cd8cef5..dee2d47 100644 --- a/src/Entity/Blog/Post.php +++ b/src/Entity/Blog/Post.php @@ -19,8 +19,8 @@ class Post implements EntityInterface { use Timestampable; - const DRAFT = 0; - const PUBLISHED = 1; + public const DRAFT = 0; + public const PUBLISHED = 1; #[ORM\Id] #[ORM\GeneratedValue] @@ -87,11 +87,21 @@ class Post implements EntityInterface #[ORM\Column(type: 'string', length: 255, nullable: true)] private $image2; + #[ORM\Column(type: 'array')] + private $parameters = []; + + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'deprecatedPosts')] + private $recommandedPost; + + #[ORM\OneToMany(mappedBy: 'recommandedPost', targetEntity: self::class)] + private $deprecatedPosts; + public function __construct() { $this->categories = new ArrayCollection(); $this->comments = new ArrayCollection(); $this->postFollows = new ArrayCollection(); + $this->deprecatedPosts = new ArrayCollection(); } public function getId(): ?int @@ -192,7 +202,7 @@ class Post implements EntityInterface } /** - * @return Collection|Category[] + * @return Category[]|Collection */ public function getCategories(): Collection { @@ -438,4 +448,80 @@ class Post implements EntityInterface return $this; } + + public function getParameters(): ?array + { + $params = is_array($this->parameters) ? $this->parameters : []; + $names = array_map(fn (array $param): string => $param['name'], $params); + $defaultParams = [ + ['name' => 'podcast', 'value' => 0], + ]; + + foreach ($defaultParams as $defaultParam) { + if (!in_array($defaultParam['name'], $names)) { + $params[] = $defaultParam; + } + } + + return $params; + } + + public function setParameters(array $parameters): self + { + $this->parameters = $parameters; + + return $this; + } + + public function getParameter($name): ?string + { + return array_filter( + $this->getParameters(), + function (array $param) use ($name): bool { + return $name === $param['name']; + } + )[0]['value'] ?? null; + } + + public function getRecommandedPost(): ?self + { + return $this->recommandedPost; + } + + public function setRecommandedPost(?self $recommandedPost): self + { + $this->recommandedPost = $recommandedPost; + + return $this; + } + + /** + * @return Collection + */ + public function getDeprecatedPosts(): Collection + { + return $this->deprecatedPosts; + } + + public function addDeprecatedPost(self $deprecatedPost): self + { + if (!$this->deprecatedPosts->contains($deprecatedPost)) { + $this->deprecatedPosts[] = $deprecatedPost; + $deprecatedPost->setRecommandedPost($this); + } + + return $this; + } + + public function removeDeprecatedPost(self $deprecatedPost): self + { + if ($this->deprecatedPosts->removeElement($deprecatedPost)) { + // set the owning side to null (unless already changed) + if ($deprecatedPost->getRecommandedPost() === $this) { + $deprecatedPost->setRecommandedPost(null); + } + } + + return $this; + } } diff --git a/src/Entity/Page/CategoriesPage.php b/src/Entity/Page/CategoriesPage.php index b669dfd..76fc480 100644 --- a/src/Entity/Page/CategoriesPage.php +++ b/src/Entity/Page/CategoriesPage.php @@ -5,6 +5,4 @@ namespace App\Entity\Page; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] -class CategoriesPage extends TitledPage -{ -} +class CategoriesPage extends TitledPage {} diff --git a/src/Entity/Page/CategoryPage.php b/src/Entity/Page/CategoryPage.php index 7b62256..a6cc5d9 100644 --- a/src/Entity/Page/CategoryPage.php +++ b/src/Entity/Page/CategoryPage.php @@ -5,6 +5,4 @@ namespace App\Entity\Page; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] -class CategoryPage extends TitledPage -{ -} +class CategoryPage extends TitledPage {} diff --git a/src/Entity/Page/ContactPage.php b/src/Entity/Page/ContactPage.php index 1592844..a2f78d9 100644 --- a/src/Entity/Page/ContactPage.php +++ b/src/Entity/Page/ContactPage.php @@ -5,6 +5,4 @@ namespace App\Entity\Page; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] -class ContactPage extends SimplePage -{ -} +class ContactPage extends SimplePage {} diff --git a/src/Entity/Page/LinksPage.php b/src/Entity/Page/LinksPage.php index 86a04cb..bfab91b 100644 --- a/src/Entity/Page/LinksPage.php +++ b/src/Entity/Page/LinksPage.php @@ -5,6 +5,4 @@ namespace App\Entity\Page; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] -class LinksPage extends SimplePage -{ -} +class LinksPage extends SimplePage {} diff --git a/src/Entity/Page/MeshPage.php b/src/Entity/Page/MeshPage.php index 68d9eae..0f41bf8 100644 --- a/src/Entity/Page/MeshPage.php +++ b/src/Entity/Page/MeshPage.php @@ -5,6 +5,4 @@ namespace App\Entity\Page; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] -class MeshPage extends SimplePage -{ -} +class MeshPage extends SimplePage {} diff --git a/src/Entity/Page/PostPage.php b/src/Entity/Page/PostPage.php index 993da27..dd46861 100644 --- a/src/Entity/Page/PostPage.php +++ b/src/Entity/Page/PostPage.php @@ -5,6 +5,4 @@ namespace App\Entity\Page; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] -class PostPage extends TitledPage -{ -} +class PostPage extends TitledPage {} diff --git a/src/Entity/Page/PostsPage.php b/src/Entity/Page/PostsPage.php index a986aff..a56d5fb 100644 --- a/src/Entity/Page/PostsPage.php +++ b/src/Entity/Page/PostsPage.php @@ -5,6 +5,4 @@ namespace App\Entity\Page; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] -class PostsPage extends TitledPage -{ -} +class PostsPage extends TitledPage {} diff --git a/src/Entity/Page/SearchPage.php b/src/Entity/Page/SearchPage.php index fa8648d..3e25764 100644 --- a/src/Entity/Page/SearchPage.php +++ b/src/Entity/Page/SearchPage.php @@ -5,6 +5,4 @@ namespace App\Entity\Page; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] -class SearchPage extends TitledPage -{ -} +class SearchPage extends TitledPage {} diff --git a/src/Entity/Page/SimplePage.php b/src/Entity/Page/SimplePage.php index 5663740..e98493a 100644 --- a/src/Entity/Page/SimplePage.php +++ b/src/Entity/Page/SimplePage.php @@ -4,7 +4,7 @@ namespace App\Entity\Page; use App\Core\Entity\Site\Page\Block; use App\Core\Entity\Site\Page\FileBlock; -use App\Form\Type\SimpleMdTextareaBlockType; +use App\Form\MarkdownBlockType; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Form\FormBuilderInterface; @@ -17,7 +17,7 @@ class SimplePage extends TitledPage $builder->add( 'content', - SimpleMdTextareaBlockType::class, + MarkdownBlockType::class, [ 'label' => 'Contenu', 'options' => [ diff --git a/src/Entity/Page/TextPage.php b/src/Entity/Page/TextPage.php index 8e754dd..58af9c6 100644 --- a/src/Entity/Page/TextPage.php +++ b/src/Entity/Page/TextPage.php @@ -3,11 +3,10 @@ namespace App\Entity\Page; use App\Core\Entity\Site\Page\Block; -use App\Core\Entity\Site\Page\FileBlock; +use App\Core\Entity\Site\Page\Page; use App\Core\Form\Site\Page\TextareaBlockType; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Form\FormBuilderInterface; -use App\Core\Entity\Site\Page\Page; #[ORM\Entity] class TextPage extends Page @@ -28,6 +27,14 @@ class TextPage extends Page ], ] ); + + $builder + ->remove('metaTitle') + ->remove('metaDescription') + ->remove('ogTitle') + ->remove('ogDescription') + ->remove('ogImage') + ; } public function setContent(Block $block) diff --git a/src/Entity/Project.php b/src/Entity/Project.php index ce86920..81db82a 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -13,8 +13,8 @@ class Project implements EntityInterface { use Timestampable; - const DRAFT = 0; - const PUBLISHED = 1; + public const DRAFT = 0; + public const PUBLISHED = 1; #[ORM\Id] #[ORM\GeneratedValue] diff --git a/src/Entity/StlMesh.php b/src/Entity/StlMesh.php index 032bec1..272e910 100644 --- a/src/Entity/StlMesh.php +++ b/src/Entity/StlMesh.php @@ -2,9 +2,9 @@ namespace App\Entity; +use App\Core\Entity\EntityInterface; use App\Repository\StlMeshRepository; use Doctrine\ORM\Mapping as ORM; -use App\Core\Entity\EntityInterface; #[ORM\Entity(repositoryClass: StlMeshRepository::class)] class StlMesh implements EntityInterface @@ -86,7 +86,7 @@ class StlMesh implements EntityInterface { $this->files = (array) $this->files; - usort($this->files, function($a, $b) { + usort($this->files, function ($a, $b) { if ($a['position'] > $b['position']) { return 1; } diff --git a/src/Entity/User.php b/src/Entity/User.php index 1aa3319..9c8a772 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -50,9 +50,7 @@ class User implements PasswordAuthenticatedUserInterface, UserInterface, TwoFact #[ORM\Column(type: 'boolean', options: ['default' => 0])] private $isWriter; - public function __construct() - { - } + public function __construct() {} public function __toString() { diff --git a/src/EventListener/StatListener.php b/src/EventListener/StatListener.php new file mode 100644 index 0000000..7248f69 --- /dev/null +++ b/src/EventListener/StatListener.php @@ -0,0 +1,22 @@ + + */ +class StatListener +{ + public function __construct(protected MessageBusInterface $bus) {} + + public function onKernelRequest(RequestEvent $event) + { + $this->bus->dispatch(new PageViewMessage(time())); + } +} diff --git a/src/EventSubscriber/SettingEventSubscriber.php b/src/EventSubscriber/SettingEventSubscriber.php index c80235f..e571122 100644 --- a/src/EventSubscriber/SettingEventSubscriber.php +++ b/src/EventSubscriber/SettingEventSubscriber.php @@ -4,11 +4,12 @@ namespace App\EventSubscriber; use App\Core\Event\Setting\SettingEvent; use App\Core\EventSubscriber\SettingEventSubscriber as EventSubscriber; +use App\Core\Form\FileManager\FilePickerType; +use App\Core\Form\Type\TinymceTextareaType; use App\Core\Setting\SettingManager; use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextType; -use App\Core\Form\FileManager\FilePickerType; /** * class SettingEventSubscriber. @@ -36,6 +37,7 @@ class SettingEventSubscriber extends EventSubscriber $this->manager->init('stats_umami_url', '📊 Statistiques', 'Adresse tableau de bord Umami', ''); $this->manager->init('stats_umami_tag', '📊 Statistiques', 'Script Umami', ''); + $this->manager->init('stats_grafana_url', '📊 Statistiques', 'Adresse tableau de bord Grafana', ''); $this->manager->init('post_author_description', '🖊️ Article', 'Description auteur', ''); @@ -68,7 +70,7 @@ class SettingEventSubscriber extends EventSubscriber ); } - if (in_array($entity->getCode(), ['giphy_api_key', 'stats_umami_url'])) { + if (in_array($entity->getCode(), ['giphy_api_key', 'stats_umami_url', 'stats_grafana_url'])) { $builder->add( 'value', TextType::class, @@ -93,7 +95,7 @@ class SettingEventSubscriber extends EventSubscriber ); } - if (in_array($entity->getCode(), ['blog_footer', 'post_author_description'])) { + if (in_array($entity->getCode(), ['post_author_description'])) { $event->setOption('view', 'large'); $builder->add( @@ -107,5 +109,20 @@ class SettingEventSubscriber extends EventSubscriber ] ); } + + if (in_array($entity->getCode(), ['blog_footer'])) { + $event->setOption('view', 'large'); + + $builder->add( + 'value', + TinymceTextareaType::class, + [ + 'label' => $entity->getLabel(), + 'attr' => [ + 'rows' => 20, + ], + ] + ); + } } } diff --git a/src/Factory/UserFactory.php b/src/Factory/UserFactory.php index 5103b52..0da4db7 100644 --- a/src/Factory/UserFactory.php +++ b/src/Factory/UserFactory.php @@ -9,6 +9,4 @@ use App\Core\Factory\UserFactory as BaseUserFactory; * * @author Simon Vieille */ -class UserFactory extends BaseUserFactory -{ -} +class UserFactory extends BaseUserFactory {} diff --git a/src/Form/Blog/CategoryType.php b/src/Form/Blog/CategoryType.php index 1030202..e9c5e1e 100644 --- a/src/Form/Blog/CategoryType.php +++ b/src/Form/Blog/CategoryType.php @@ -3,6 +3,8 @@ namespace App\Form\Blog; use App\Entity\Blog\Category; +use Doctrine\ORM\EntityRepository; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; @@ -10,8 +12,6 @@ use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints\NotBlank; -use Doctrine\ORM\EntityRepository; -use Symfony\Bridge\Doctrine\Form\Type\EntityType; class CategoryType extends AbstractType { diff --git a/src/Form/Blog/PostParameterType.php b/src/Form/Blog/PostParameterType.php new file mode 100644 index 0000000..d29a215 --- /dev/null +++ b/src/Form/Blog/PostParameterType.php @@ -0,0 +1,47 @@ +add( + 'name', + TextType::class, + [ + 'required' => true, + 'attr' => [ + ], + 'constraints' => [ + new NotBlank(), + ], + ] + ); + + $builder->add( + 'value', + TextType::class, + [ + 'required' => false, + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => null, + ]); + } +} diff --git a/src/Form/Blog/PostType.php b/src/Form/Blog/PostType.php index e2e3104..63284ff 100644 --- a/src/Form/Blog/PostType.php +++ b/src/Form/Blog/PostType.php @@ -2,8 +2,13 @@ namespace App\Form\Blog; +use App\Core\Form\FileManager\FilePickerType; +use App\Core\Form\Type\BuilderType; +use App\Core\Form\Type\CollectionType as MurphCollectionType; +use App\Core\Form\Type\EditorJsTextareaType; use App\Entity\Blog\Category; use App\Entity\Blog\Post; +use App\Form\MarkdownType; use Doctrine\ORM\EntityRepository; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; @@ -21,9 +26,6 @@ use Symfony\Component\Validator\Constraints\Image; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\Range; use Symfony\Component\Validator\Constraints\Url; -use App\Form\Type\SimpleMdTextareaType; -use App\Core\Form\Type\EditorJsTextareaType; -use App\Core\Form\FileManager\FilePickerType; class PostType extends AbstractType { @@ -50,6 +52,7 @@ class PostType extends AbstractType 'required' => true, 'choices' => [ 'Markdown' => 'markdown', + 'Builder' => 'builder', 'HTML' => 'html', 'Editor JS' => 'editorjs', ], @@ -60,8 +63,9 @@ class PostType extends AbstractType ); $types = [ - 'markdown' => SimpleMdTextareaType::class, - 'html' => SimpleMdTextareaType::class, + 'markdown' => MarkdownType::class, + 'builder' => BuilderType::class, + 'html' => MarkdownType::class, 'editorjs' => EditorJsTextareaType::class, ]; @@ -112,6 +116,28 @@ class PostType extends AbstractType ] ); + $builder->add( + 'recommandedPost', + EntityType::class, + [ + 'label' => 'Article recommandé', + 'class' => Post::class, + 'choice_label' => 'title', + 'required' => false, + 'multiple' => false, + 'attr' => [ + 'data-jschoice' => '', + ], + 'query_builder' => function (EntityRepository $repo) { + return $repo->createQueryBuilder('a') + ->orderBy('a.title', 'ASC') + ; + }, + 'constraints' => [ + ], + ] + ); + $builder->add( 'status', ChoiceType::class, @@ -171,6 +197,7 @@ class PostType extends AbstractType 'attr' => [ ], 'constraints' => [ + new Image(), ], ] ); @@ -298,6 +325,20 @@ class PostType extends AbstractType ], ] ); + + // $builder->add( + // 'parameters', + // MurphCollectionType::class, + // [ + // 'label' => 'Paramètres', + // 'entry_type' => PostParameterType::class, + // 'by_reference' => false, + // 'allow_add' => true, + // 'allow_delete' => true, + // 'prototype' => true, + // 'row_attr' => ['class' => 'mb-3'], + // ] + // ); } public function configureOptions(OptionsResolver $resolver) diff --git a/src/Form/Blog/UserCommentType.php b/src/Form/Blog/UserCommentType.php index d800de4..b060eb1 100644 --- a/src/Form/Blog/UserCommentType.php +++ b/src/Form/Blog/UserCommentType.php @@ -3,7 +3,9 @@ namespace App\Form\Blog; use App\Entity\Blog\Comment; +use Gregwar\CaptchaBundle\Type\CaptchaType; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; @@ -14,8 +16,6 @@ use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\Url; -use Symfony\Component\Form\Extension\Core\Type\CheckboxType; -use Gregwar\CaptchaBundle\Type\CaptchaType; class UserCommentType extends AbstractType { diff --git a/src/Form/ContactType.php b/src/Form/ContactType.php index d955ef3..6c279aa 100644 --- a/src/Form/ContactType.php +++ b/src/Form/ContactType.php @@ -2,6 +2,7 @@ namespace App\Form; +use Gregwar\CaptchaBundle\Type\CaptchaType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; @@ -10,7 +11,6 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\NotBlank; -use Gregwar\CaptchaBundle\Type\CaptchaType; class ContactType extends AbstractType { diff --git a/src/Form/MarkdownBlockType.php b/src/Form/MarkdownBlockType.php new file mode 100644 index 0000000..0203464 --- /dev/null +++ b/src/Form/MarkdownBlockType.php @@ -0,0 +1,21 @@ +add( + 'value', + MarkdownType::class, + array_merge([ + 'required' => false, + 'label' => false, + ], $options['options']), + ); + } +} diff --git a/src/Form/MarkdownType.php b/src/Form/MarkdownType.php new file mode 100644 index 0000000..6ae9d11 --- /dev/null +++ b/src/Form/MarkdownType.php @@ -0,0 +1,13 @@ + 'Image', - 'required' => false, + 'required' => true, 'data_class' => null, 'attr' => [ ], 'constraints' => [ + new NotBlank(), ], ] ); diff --git a/src/Form/StlFileType.php b/src/Form/StlFileType.php index 28ece0b..f3cf300 100644 --- a/src/Form/StlFileType.php +++ b/src/Form/StlFileType.php @@ -2,13 +2,13 @@ namespace App\Form; +use App\Core\Form\FileManager\FilePickerType; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\HiddenType; +use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use App\Core\Form\FileManager\FilePickerType; -use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Form\Extension\Core\Type\TextType; class StlFileType extends AbstractType { diff --git a/src/Form/Type/SimpleMdTextareaType.php b/src/Form/Type/SimpleMdTextareaType.php index edbe163..34145d3 100644 --- a/src/Form/Type/SimpleMdTextareaType.php +++ b/src/Form/Type/SimpleMdTextareaType.php @@ -8,9 +8,6 @@ use Symfony\Component\Form\FormView; class SimpleMdTextareaType extends TextareaType { - /** - * {@inheritdoc} - */ public function buildView(FormView $view, FormInterface $form, array $options) { if (!isset($view->vars['attr']['data-simplemde'])) { diff --git a/src/Form/UserType.php b/src/Form/UserType.php index 65e372a..ac1ddd1 100644 --- a/src/Form/UserType.php +++ b/src/Form/UserType.php @@ -4,6 +4,4 @@ namespace App\Form; use App\Core\Form\UserType as BaseUserType; -class UserType extends BaseUserType -{ -} +class UserType extends BaseUserType {} diff --git a/src/Kernel.php b/src/Kernel.php index 655e796..58758a4 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -2,7 +2,9 @@ namespace App; +use App\Core\DependencyInjection\Compiler\BuilderBlockPass; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\HttpKernel\Kernel as BaseKernel; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; @@ -35,4 +37,9 @@ class Kernel extends BaseKernel (require $path)($routes->withPath($path), $this); } } + + protected function build(ContainerBuilder $container): void + { + $container->addCompilerPass(new BuilderBlockPass()); + } } diff --git a/src/Markdown/Parser/Post.php b/src/Markdown/Parser/Post.php index 8966921..f8af0e9 100644 --- a/src/Markdown/Parser/Post.php +++ b/src/Markdown/Parser/Post.php @@ -11,15 +11,9 @@ use Knp\Bundle\MarkdownBundle\Parser\MarkdownParser; */ class Post extends MarkdownParser { - /** - * {@inheritdoc} - */ - //protected $id_class_attr_catch_re = '\{((?'.'>[ ]*[#.a-z][-_:a-zA-Z0-9="\'\/\*-]+){1,})[ ]*\}'; + // protected $id_class_attr_catch_re = '\{((?'.'>[ ]*[#.a-z][-_:a-zA-Z0-9="\'\/\*-]+){1,})[ ]*\}'; protected $id_class_attr_catch_re = '\{([^\}]+)\}'; - /** - * {@inheritdoc} - */ public function transformMarkdown($text) { $html = parent::transformMarkdown($text); @@ -27,9 +21,6 @@ class Post extends MarkdownParser return str_replace(' role="doc-endnote"', '', $html); } - /** - * {@inheritdoc} - */ protected function doExtraAttributes($tag_name, $attr, $defaultIdValue = null, $classes = []) { if (empty($attr) && !$defaultIdValue && empty($classes)) { @@ -102,8 +93,11 @@ class Post extends MarkdownParser $codeblock ); - $codeblock = preg_replace_callback('/^\n+/', - [$this, '_doFencedCodeBlocks_newlines'], $codeblock); + $codeblock = preg_replace_callback( + '/^\n+/', + [$this, '_doFencedCodeBlocks_newlines'], + $codeblock + ); $classes = []; if ('' != $classname) { @@ -115,7 +109,12 @@ class Post extends MarkdownParser $attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? 'pre' : 'code', $attrs, null, $classes); $pre_attr_str = $this->code_attr_on_pre ? $attr_str : ''; $code_attr_str = $this->code_attr_on_pre ? '' : $attr_str; - $codeblock = "{$codeblock}"; + + if (preg_match('/mermaid/', $code_attr_str)) { + $codeblock = "{$codeblock}"; + } else { + $codeblock = "{$codeblock}"; + } return "\n\n".$this->hashBlock($codeblock)."\n\n"; } diff --git a/src/Message/PageViewMessage.php b/src/Message/PageViewMessage.php new file mode 100644 index 0000000..50eace5 --- /dev/null +++ b/src/Message/PageViewMessage.php @@ -0,0 +1,13 @@ +time; + } +} diff --git a/src/MessageHandler/PageViewMessageHandler.php b/src/MessageHandler/PageViewMessageHandler.php new file mode 100644 index 0000000..ac4e0d0 --- /dev/null +++ b/src/MessageHandler/PageViewMessageHandler.php @@ -0,0 +1,35 @@ +influxDB->isAvailable()) { + return; + } + + $client = $this->influxDB->getClient(); + + $writeApi = $client->createWriteApi(['writeType' => WriteType::SYNCHRONOUS]); + $pageView = new Point('page_view'); + $pageView + ->addTag('request', 'view') + ->addField('value', 1) + ->time($message->getTime()) + ; + + $writeApi->write($pageView); + $writeApi->close(); + $client->close(); + } +} diff --git a/src/Middleware/PageViewMiddleware.php b/src/Middleware/PageViewMiddleware.php new file mode 100644 index 0000000..1944109 --- /dev/null +++ b/src/Middleware/PageViewMiddleware.php @@ -0,0 +1,16 @@ +next()->handle($envelope, $stack); + } +} diff --git a/src/Repository/AppEntityBlogPostRepository.php b/src/Repository/AppEntityBlogPostRepository.php new file mode 100644 index 0000000..51e8e0c --- /dev/null +++ b/src/Repository/AppEntityBlogPostRepository.php @@ -0,0 +1,66 @@ + + * + * @method null|AppEntityBlogPost find($id, $lockMode = null, $lockVersion = null) + * @method null|AppEntityBlogPost findOneBy(array $criteria, array $orderBy = null) + * @method AppEntityBlogPost[] findAll() + * @method AppEntityBlogPost[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class AppEntityBlogPostRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, AppEntityBlogPost::class); + } + + public function add(AppEntityBlogPost $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(AppEntityBlogPost $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + // /** + // * @return AppEntityBlogPost[] Returns an array of AppEntityBlogPost objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('a') + // ->andWhere('a.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('a.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?AppEntityBlogPost + // { + // return $this->createQueryBuilder('a') + // ->andWhere('a.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Repository/Blog/CommentRepository.php b/src/Repository/Blog/CommentRepository.php index 306d21f..b111fc7 100644 --- a/src/Repository/Blog/CommentRepository.php +++ b/src/Repository/Blog/CommentRepository.php @@ -7,8 +7,8 @@ use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; /** - * @method Comment|null find($id, $lockMode = null, $lockVersion = null) - * @method Comment|null findOneBy(array $criteria, array $orderBy = null) + * @method null|Comment find($id, $lockMode = null, $lockVersion = null) + * @method null|Comment findOneBy(array $criteria, array $orderBy = null) * @method Comment[] findAll() * @method Comment[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ diff --git a/src/Repository/Blog/PostFollowRepositoryQuery.php b/src/Repository/Blog/PostFollowRepositoryQuery.php index a14240c..f528a63 100644 --- a/src/Repository/Blog/PostFollowRepositoryQuery.php +++ b/src/Repository/Blog/PostFollowRepositoryQuery.php @@ -3,8 +3,8 @@ namespace App\Repository\Blog; use App\Core\Repository\RepositoryQuery; -use Knp\Component\Pager\PaginatorInterface; use App\Repository\Blog\PostFollowRepository as Repository; +use Knp\Component\Pager\PaginatorInterface; class PostFollowRepositoryQuery extends RepositoryQuery { diff --git a/src/Repository/Blog/PostRepositoryQuery.php b/src/Repository/Blog/PostRepositoryQuery.php index 0def03f..c8db9e6 100644 --- a/src/Repository/Blog/PostRepositoryQuery.php +++ b/src/Repository/Blog/PostRepositoryQuery.php @@ -54,15 +54,13 @@ class PostRepositoryQuery extends RepositoryQuery ; } - protected function filterHandler(string $name, $value) - { - if ('category' === $name) { - $this->inCategory($value); - } - } - public function search(?string $keywords, ?string $tag) { + $keywords = explode(' ', $keywords); + + $filterWords = fn ($keyword) => '' !== trim($keyword) && preg_match('/[a-zA-Z]+/', $keyword); + $keywords = array_filter($keywords, $filterWords); + if ($keywords) { $conn = $this->repository->getEm()->getConnection(); @@ -70,43 +68,107 @@ class PostRepositoryQuery extends RepositoryQuery 'SELECT post.id, post.title, - MATCH(post.title) AGAINST(:search) AS MATCH_TITLE, - MATCH(post.content) AGAINST(:search) AS MATCH_CONTENT + post.content, + post.published_at FROM post WHERE post.status = 1 AND post.published_at < :date - ORDER BY - MATCH_TITLE DESC, - MATCH_CONTENT DESC - '); + ' + ); $statement = $query->execute([ - ':search' => $keywords, ':date' => (new \DateTime())->format('Y-m-d H:i:s'), ]); $results = $statement->fetchAll(); $ids = []; + $matches = []; foreach ($results as $k => $v) { - $rate = ($v['MATCH_TITLE'] * 2) + $v['MATCH_CONTENT']; + $initWords = explode(' ', $v['title']); + $words = []; - if ($rate >= 7) { - $ids[] = $v['id']; + foreach ($initWords as $initWord) { + $words = array_merge($words, preg_split('/[:_\'-]+/', $initWord)); } - } - if (0 == count($ids)) { - foreach ($results as $k => $v) { - $rate = ($v['MATCH_TITLE'] * 2) + $v['MATCH_CONTENT']; + $words = array_filter($words, $filterWords); - if ($rate >= 6) { - $ids[] = $v['id']; + foreach ($keywords as $keyword) { + if (str_contains(mb_strtolower($v['content']), mb_strtolower($keyword))) { + $similarity = 99; + + if (isset($matches[$v['id']])) { + $matches[$v['id']]['similarity'] += $similarity; + ++$matches[$v['id']]['count']; + } else { + $matches[$v['id']] = [ + 'id' => $v['id'], + 'title' => $v['title'], + 'published_at' => $v['published_at'], + 'similarity' => $similarity, + 'count' => 1, + ]; + } + } + + foreach ($words as $word) { + if (str_contains(mb_strtolower($word), mb_strtolower($keyword))) { + $similarity = 150; + + if (isset($matches[$v['id']])) { + $matches[$v['id']]['similarity'] += $similarity; + ++$matches[$v['id']]['count']; + } else { + $matches[$v['id']] = [ + 'id' => $v['id'], + 'title' => $v['title'], + 'published_at' => $v['published_at'], + 'similarity' => $similarity, + 'count' => 1, + ]; + } + } else { + $lev = levenshtein($word, $keyword); + $similarity = 100 - ($lev * 100 / mb_strlen($word)); + + if ($similarity > 70) { + if (isset($matches[$v['id']])) { + $matches[$v['id']]['similarity'] += $similarity; + } else { + $matches[$v['id']] = [ + 'id' => $v['id'], + 'title' => $v['title'], + 'published_at' => $v['published_at'], + 'similarity' => $similarity, + 'count' => 1, + ]; + } + } + } } } } + $matches = array_filter($matches, function ($match) use ($keywords) { + return (100 * $match['count'] / count($keywords)) > 80; + }); + + usort($matches, function ($a, $b) { + if ($a['similarity'] > $b['similarity']) { + return -1; + } + + if ($b['similarity'] > $a['similarity']) { + return 1; + } + + return ($a['published_at'] != $b['published_at']) * -1; + }); + + $ids = array_column($matches, 'id'); + if (!$ids) { $ids = [-1]; } @@ -127,4 +189,11 @@ class PostRepositoryQuery extends RepositoryQuery return $this; } + + protected function filterHandler(string $name, $value) + { + if ('category' === $name) { + $this->inCategory($value); + } + } } diff --git a/src/Repository/ProjectRepository.php b/src/Repository/ProjectRepository.php index 560cb87..8801364 100644 --- a/src/Repository/ProjectRepository.php +++ b/src/Repository/ProjectRepository.php @@ -9,8 +9,8 @@ use Doctrine\ORM\ORMException; use Doctrine\Persistence\ManagerRegistry; /** - * @method Project|null find($id, $lockMode = null, $lockVersion = null) - * @method Project|null findOneBy(array $criteria, array $orderBy = null) + * @method null|Project find($id, $lockMode = null, $lockVersion = null) + * @method null|Project findOneBy(array $criteria, array $orderBy = null) * @method Project[] findAll() * @method Project[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ diff --git a/src/Repository/ProjectRepositoryQuery.php b/src/Repository/ProjectRepositoryQuery.php index b4fa818..49ae4ad 100644 --- a/src/Repository/ProjectRepositoryQuery.php +++ b/src/Repository/ProjectRepositoryQuery.php @@ -3,9 +3,9 @@ namespace App\Repository; use App\Core\Repository\RepositoryQuery; -use Knp\Component\Pager\PaginatorInterface; -use App\Repository\ProjectRepository as Repository; use App\Entity\Project; +use App\Repository\ProjectRepository as Repository; +use Knp\Component\Pager\PaginatorInterface; class ProjectRepositoryQuery extends RepositoryQuery { diff --git a/src/Repository/StlMeshRepository.php b/src/Repository/StlMeshRepository.php index ede601b..4fa6b4d 100644 --- a/src/Repository/StlMeshRepository.php +++ b/src/Repository/StlMeshRepository.php @@ -7,8 +7,8 @@ use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; /** - * @method StlMesh|null find($id, $lockMode = null, $lockVersion = null) - * @method StlMesh|null findOneBy(array $criteria, array $orderBy = null) + * @method null|StlMesh find($id, $lockMode = null, $lockVersion = null) + * @method null|StlMesh findOneBy(array $criteria, array $orderBy = null) * @method StlMesh[] findAll() * @method StlMesh[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ diff --git a/src/Repository/StlMeshRepositoryQuery.php b/src/Repository/StlMeshRepositoryQuery.php index 25c4bbe..95d93a2 100644 --- a/src/Repository/StlMeshRepositoryQuery.php +++ b/src/Repository/StlMeshRepositoryQuery.php @@ -3,8 +3,8 @@ namespace App\Repository; use App\Core\Repository\RepositoryQuery; -use Knp\Component\Pager\PaginatorInterface; use App\Repository\StlMeshRepository as Repository; +use Knp\Component\Pager\PaginatorInterface; class StlMeshRepositoryQuery extends RepositoryQuery { diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index 570efd5..39ecbe4 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -6,9 +6,8 @@ use App\Entity\User; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; -use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; -use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; +use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface { diff --git a/src/Twig/Extension/BlogExtension.php b/src/Twig/Extension/BlogExtension.php index 3b1467a..102099d 100644 --- a/src/Twig/Extension/BlogExtension.php +++ b/src/Twig/Extension/BlogExtension.php @@ -40,8 +40,9 @@ class BlogExtension extends AbstractExtension $text = str_replace('http://upload.deblan.fr', 'https://upload.deblan.org', $text); $text = str_replace('http://dedi.geneweb.fr', 'http://kim.deblan.fr', $text); $text = str_replace('http://mediaplayer.deblan.fr', 'https://mediaplayer.deblan.org', $text); - $text = str_replace('http://blog.deblan.fr', 'https://www.deblan.io', $text); - $text = str_replace('http://www.deblan.tv', 'https://www.deblan.io', $text); + $text = str_replace('http://blog.deblan.fr', 'https://www.deblan.fr', $text); + $text = str_replace('http://www.deblan.tv', 'https://www.deblan.fr', $text); + $text = str_replace('http://www.deblan.io', 'https://www.deblan.fr', $text); $text = preg_replace_callback( '`]*)>(.*)]*)>`isU', @@ -134,10 +135,12 @@ class BlogExtension extends AbstractExtension '#(.*)#isU', ], function ($data) { - $lang = strtolower(str_replace( - ['console', 'texte', 'apache'], - ['bash', 'text', 'html'], - $data[1]) + $lang = strtolower( + str_replace( + ['console', 'texte', 'apache'], + ['bash', 'text', 'html'], + $data[1] + ) ); $class = 'language-'.$lang; diff --git a/src/Twig/Extension/ColorExtension.php b/src/Twig/Extension/ColorExtension.php index 09311bc..f39a3f6 100644 --- a/src/Twig/Extension/ColorExtension.php +++ b/src/Twig/Extension/ColorExtension.php @@ -30,9 +30,9 @@ class ColorExtension extends AbstractExtension return sprintf( '#%s%s%s', - strlen($red) != 2 ? '0'.$red : $red, - strlen($green) != 2 ? '0'.$green : $green, - strlen($blue) != 2 ? '0'.$blue : $blue, + 2 != strlen($red) ? '0'.$red : $red, + 2 != strlen($green) ? '0'.$green : $green, + 2 != strlen($blue) ? '0'.$blue : $blue, ); } } diff --git a/src/Twig/Extension/LazyLoadExtension.php b/src/Twig/Extension/LazyLoadExtension.php index 167d12a..59a660a 100644 --- a/src/Twig/Extension/LazyLoadExtension.php +++ b/src/Twig/Extension/LazyLoadExtension.php @@ -21,7 +21,7 @@ class LazyLoadExtension extends AbstractExtension public function lazyLoad($text) { - $text = preg_replace_callback( + return preg_replace_callback( '`([^`isU', function ($data) { $lazy = sprintf('%s', $data[1], $data[2]); @@ -31,7 +31,5 @@ class LazyLoadExtension extends AbstractExtension }, $text ); - - return $text; } } diff --git a/src/Twig/Extension/TypoExtension.php b/src/Twig/Extension/TypoExtension.php index 132f8eb..ad00faa 100644 --- a/src/Twig/Extension/TypoExtension.php +++ b/src/Twig/Extension/TypoExtension.php @@ -4,7 +4,6 @@ namespace App\Twig\Extension; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; -use Twig\TwigFunction; class TypoExtension extends AbstractExtension { diff --git a/src/UrlGenerator/PostGenerator.php b/src/UrlGenerator/PostGenerator.php index b0e6bd5..8d3f485 100644 --- a/src/UrlGenerator/PostGenerator.php +++ b/src/UrlGenerator/PostGenerator.php @@ -3,7 +3,6 @@ namespace App\UrlGenerator; use App\Core\Entity\Site\Node; -use App\Entity\Blog\Category; use App\Repository\Blog\CategoryRepositoryQuery; use App\Repository\Blog\PostRepositoryQuery; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; diff --git a/templates/_form.html.twig b/templates/_form.html.twig new file mode 100644 index 0000000..ed04750 --- /dev/null +++ b/templates/_form.html.twig @@ -0,0 +1 @@ +{{ include('@Core/user/user_admin/_form.html.twig') }} diff --git a/templates/_show.html.twig b/templates/_show.html.twig new file mode 100644 index 0000000..806f07d --- /dev/null +++ b/templates/_show.html.twig @@ -0,0 +1 @@ +{{ include('@Core/user/user_admin/_show.html.twig') }} diff --git a/templates/admin/dashboard.html.twig b/templates/admin/dashboard.html.twig index 79edf51..46861e7 100644 --- a/templates/admin/dashboard.html.twig +++ b/templates/admin/dashboard.html.twig @@ -4,6 +4,72 @@ {% block body %}
- +
+

Bienvenue, {{ app.user.displayName }} đź‘‹

+
+
+ +
+
+ + {% for entity in posts %} +
+ {{ include('blog/post_admin/field/title.html.twig') }} +
+ {% endfor %} +
+ +
+ + {% for entity in projects %} + + {% endfor %} +
+
+ +
+ + +
+
+ +
+
+ +
+
+
{% endblock %} diff --git a/templates/admin/user_admin/UserAdminController.php b/templates/admin/user_admin/UserAdminController.php new file mode 100644 index 0000000..9970f51 --- /dev/null +++ b/templates/admin/user_admin/UserAdminController.php @@ -0,0 +1,79 @@ + '\d+'])] + public function index(RepositoryQuery $query, Request $request, Session $session, int $page = 1): Response + { + return parent::index($query, $request, $session, $page); + } + + #[Route(path: '/admin/user/new', name: 'admin_user_new', methods: ['GET', 'POST'])] + public function new(Factory $factory, EntityManager $entityManager, Request $request, TokenGenerator $tokenGenerator): Response + { + return parent::new($factory, $entityManager, $request, $tokenGenerator); + } + + #[Route(path: '/admin/user/show/{entity}', name: 'admin_user_show', methods: ['GET'])] + public function show(Entity $entity): Response + { + return parent::show($entity); + } + + #[Route(path: '/admin/user/filter', name: 'admin_user_filter', methods: ['GET'])] + public function filter(Session $session): Response + { + return parent::filter($session); + } + + #[Route(path: '/admin/user/edit/{entity}', name: 'admin_user_edit', methods: ['GET', 'POST'])] + public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response + { + return parent::edit($entity, $entityManager, $request); + } + + #[Route(path: '/admin/user/inline_edit/{entity}/{context}/{label}', name: 'admin_user_inline_edit', methods: ['GET', 'POST'])] + public function inlineEdit(string $context, string $label, Entity $entity, EntityManager $entityManager, Request $request): Response + { + return parent::inlineEdit($context, $label, $entity, $entityManager, $request); + } + + #[Route(path: '/admin/user/delete/{entity}', name: 'admin_user_delete', methods: ['DELETE', 'POST'])] + public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response + { + return parent::delete($entity, $entityManager, $request); + } + + #[Route(path: '/admin/user/resetting_request/{entity}', name: 'admin_user_resetting_request', methods: ['POST'])] + public function requestResetting(Entity $entity, EventDispatcherInterface $eventDispatcher, Request $request): Response + { + return parent::requestResetting($entity, $eventDispatcher, $request); + } + + protected function getConfiguration(): CrudConfiguration + { + if ($this->configuration) { + return $this->configuration; + } + + return parent::getConfiguration() + ->setView('form', 'admin/user_admin/_form.html.twig') + ->setView('show_entity', 'admin/user_admin/_show.html.twig') + ; + } +} diff --git a/templates/admin/user_admin/_form.html.twig b/templates/admin/user_admin/_form.html.twig new file mode 100644 index 0000000..ed04750 --- /dev/null +++ b/templates/admin/user_admin/_form.html.twig @@ -0,0 +1 @@ +{{ include('@Core/user/user_admin/_form.html.twig') }} diff --git a/templates/admin/user_admin/_show.html.twig b/templates/admin/user_admin/_show.html.twig new file mode 100644 index 0000000..806f07d --- /dev/null +++ b/templates/admin/user_admin/_show.html.twig @@ -0,0 +1 @@ +{{ include('@Core/user/user_admin/_show.html.twig') }} diff --git a/templates/base.html.twig b/templates/base.html.twig index 68fff2e..f8d1fb4 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -50,6 +50,7 @@ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + {# @@ -59,15 +60,18 @@ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - - + #} + + + + {% block links_extra %}{% endblock %}
diff --git a/templates/blog/post_admin/_form.html.twig b/templates/blog/post_admin/_form.html.twig index da54000..1581f4e 100644 --- a/templates/blog/post_admin/_form.html.twig +++ b/templates/blog/post_admin/_form.html.twig @@ -1,7 +1,7 @@
- {% for item in ['categories', 'slug'] %} + {% for item in ['categories', 'slug', 'recommandedPost'] %}
{{ form_row(form[item]) }}
diff --git a/templates/blog/post_admin/_show.html.twig b/templates/blog/post_admin/_show.html.twig index f3dae48..b896c31 100644 --- a/templates/blog/post_admin/_show.html.twig +++ b/templates/blog/post_admin/_show.html.twig @@ -72,6 +72,8 @@ {% if entity.contentFormat == 'html' %} {{- entity.content|murph_url|file_attributes|post -}} + {% elseif entity.contentFormat == 'builder' %} + {{- entity.content|block_to_html|file_attributes|lazy_load -}} {% elseif entity.contentFormat == 'markdown' %} {{- entity.content|murph_url|file_attributes|markdown('post') -}} {% elseif entity.contentFormat == 'editorjs' %} diff --git a/templates/blog/post_admin/index.html.twig b/templates/blog/post_admin/index.html.twig index 38ac4fe..4ace0e1 100644 --- a/templates/blog/post_admin/index.html.twig +++ b/templates/blog/post_admin/index.html.twig @@ -3,7 +3,7 @@ {% block list_item_actions_before %} {% if configuration.action(context, 'analytic_stats', true) %} {% set analytics = path( - configuration.pageRoute('analytic_stats'), + configuration.pageRoute('analytic_stats'), {entity: item.id}|merge(configuration.pageRouteParams('analytic_stats')) ) %} diff --git a/templates/blog/post_admin/show.html.twig b/templates/blog/post_admin/show.html.twig index 815636b..37163b8 100644 --- a/templates/blog/post_admin/show.html.twig +++ b/templates/blog/post_admin/show.html.twig @@ -1,9 +1,21 @@ {% extends '@Core/admin/crud/show.html.twig' %} {% block header_actions_after %} - + Voir en ligne + {% if configuration.action(context, 'analytic_stats', true) %} + {% set analytics = path( + configuration.pageRoute('analytic_stats'), + {entity: entity.id}|merge(configuration.pageRouteParams('analytic_stats')) + ) %} + + + + Statistiques + + {% endif %} + {{ parent() }} {% endblock %} diff --git a/templates/core/admin/module/metas.html.twig b/templates/core/admin/module/metas.html.twig index 6cbfecb..affaf05 100644 --- a/templates/core/admin/module/metas.html.twig +++ b/templates/core/admin/module/metas.html.twig @@ -1,5 +1,6 @@ +{# @@ -14,7 +15,9 @@ - +#} + + diff --git a/templates/editorjs/link.html.twig b/templates/editorjs/link.html.twig index fde875d..2c1a8a2 100644 --- a/templates/editorjs/link.html.twig +++ b/templates/editorjs/link.html.twig @@ -1,5 +1,5 @@ {% block render %} -