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: ` +
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 @@ + + 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{$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(
'`