From ae440be40cdea6e972dc1bd691e519518f70ec46 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 8 Jun 2026 13:00:11 -0700 Subject: [PATCH] Add tag support --- assets/styles/main.css | 2 +- assets/styles/scss/front/_posts.scss | 24 ++++++++ migrations/Version20260608193439.php | 37 ++++++++++++ .../Brain/BrainCategoryController.php | 55 ++++++++++++++++++ .../Brain/BrainPhotosController.php | 4 ++ src/Controller/Brain/BrainPostController.php | 4 ++ src/Controller/Brain/BrainTagController.php | 56 +++++++++++++++++++ src/Controller/FrontEnd/TagController.php | 48 ++++++++++++++++ src/Entity/Photos.php | 31 ++++++++++ src/Entity/Tag.php | 49 ++++++++++++++++ src/Form/CategoryType.php | 26 +++++++++ src/Form/PhotosType.php | 2 + src/Form/PostType.php | 6 +- src/Form/TagAutocompleteField.php | 6 -- src/Form/TagType.php | 27 +++++++++ templates/brain/menu/menu.html.twig | 2 + templates/brain/photos/create.html.twig | 3 + templates/brain/post/create.html.twig | 4 +- templates/brain/taxonomy/create.html.twig | 23 ++++++++ templates/brain/taxonomy/index.html.twig | 27 +++++++++ templates/front/home/index.html.twig | 2 +- templates/front/post/detail.html.twig | 8 +++ templates/front/tag/detail.html.twig | 28 ++++++++++ templates/front/tag/index.html.twig | 16 ++++++ 24 files changed, 475 insertions(+), 15 deletions(-) create mode 100644 migrations/Version20260608193439.php create mode 100644 src/Controller/Brain/BrainCategoryController.php create mode 100644 src/Controller/Brain/BrainTagController.php create mode 100644 src/Controller/FrontEnd/TagController.php create mode 100644 src/Form/CategoryType.php create mode 100644 src/Form/TagType.php create mode 100644 templates/brain/taxonomy/create.html.twig create mode 100644 templates/brain/taxonomy/index.html.twig create mode 100644 templates/front/tag/detail.html.twig create mode 100644 templates/front/tag/index.html.twig diff --git a/assets/styles/main.css b/assets/styles/main.css index a13526b..f0c0b51 100644 --- a/assets/styles/main.css +++ b/assets/styles/main.css @@ -1 +1 @@ -/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:rgba(0,0,0,0)}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}@font-face{font-family:RobotoSlab-Regular;src:url("/fonts/RobotoSlab-Regular.ttf") format("opentype")}@font-face{font-family:RobotoSlab-Medium;src:url("/fonts/RobotoSlab-Regular.ttf") format("opentype")}@font-face{font-family:RobotoSlab-Bold;src:url("/fonts/RobotoSlab-Bold.ttf") format("opentype")}@font-face{font-family:RobotoSlab-Black;src:url("/fonts/RobotoSlab-Black.ttf") format("opentype")}@font-face{font-family:AtomicAge-Regular;src:url("/fonts/AtomicAge-Regular.ttf") format("opentype")}@font-face{font-family:Montserrat-Regular;src:url("/fonts/Montserrat-Regular.ttf") format("opentype")}@font-face{font-family:Montserrat-Light;src:url("/fonts/Montserrat-Light.ttf") format("opentype")}@font-face{font-family:Montserrat-Italic;src:url("/fonts/Montserrat-Italic.ttf") format("opentype")}@font-face{font-family:Montserrat-Bold;src:url("/fonts/Montserrat-Bold.ttf") format("opentype")}@font-face{font-family:Montserrat-SemiBold;src:url("/fonts/Montserrat-SemiBold.ttf") format("opentype")}@font-face{font-family:Orbitron-Regular;src:url("/fonts/Orbitron-Regular.ttf") format("opentype")}@font-face{font-family:Amarante;src:url("/fonts/Amarante-Regular.ttf") format("opentype")}h1,h2,h3,h4,h5,h6{font-family:Amarante;letter-spacing:2px}h1{font-size:2rem}@media screen and (min-width: 600px){h1{font-size:3rem}}h2{font-size:1.75rem}@media screen and (min-width: 600px){h2{font-size:2.5rem}}h3{font-size:1.5rem}@media screen and (min-width: 600px){h3{font-size:2rem}}h4{font-size:1.25rem}@media screen and (min-width: 600px){h4{font-size:1.5rem}}h5{font-size:1rem}@media screen and (min-width: 600px){h5{font-size:1.25rem}}h6{font-size:1rem}@media screen and (min-width: 600px){h6{font-size:1em}}p,a,li,span{font-family:RobotoSlab-Regular;letter-spacing:1px}strong{font-family:Montserrat-Bold}em{font-family:Montserrat-Italic}u{font-family:Montserrat-SemiBold}cite,q,small{font-family:Orbitron-Regular}s{font-family:Montserrat-SemiBold}h1,h2,h3,h4,h5,h6,p,a,span,time{color:#fbfcff}.secondary.layout--v1{display:block;position:relative;padding:.5rem .5rem;background-color:#d7b94c}.secondary.layout--v1 #root{display:flex;flex-direction:column}@media(min-width: 800px){.secondary.layout--v1 #root{display:grid;grid-template-columns:25% 75%;grid-template-rows:1fr;margin:0 auto;padding:1rem 1rem}}.secondary.layout--v1 #root{gap:1rem;position:relative;width:100%;height:100%;min-height:100vh;max-width:1200px;background-color:#1f2421;box-shadow:0 0 5px 10px rgba(29,31,32,.45)}.secondary.layout--v1 #root main{padding:1rem 1rem}.secondary.layout--v1.menu-open,.secondary.layout--v1.photo-open{overflow:hidden}.secondary.layout--v1.photo-open .overlay{display:block;position:absolute;top:0;left:0;width:100%;height:100%;background-color:#171a21;opacity:.9}header{display:block;position:sticky;overflow:auto;width:100%;height:50px;top:0;padding-bottom:1rem;background-color:#1f2421}@media(min-width: 600px){header{height:500px}}header.open{overflow:visible}header.open .top h1{display:none}header nav.main{display:block;position:relative}@media(min-width: 600px){header nav.main{display:flex;flex-direction:column;gap:2rem}}header nav.main .top{display:flex}header nav.main .top h1{margin:0}header nav.main .top h1 a{display:block;position:relative;top:1rem;left:1rem;font-size:1.5rem;text-decoration:none;color:#1d1f20;background-color:#d7b94c;border:2px solid #d7b94c;padding:2px 2px}header nav.main .top h1 a:hover{color:#d7b94c;background-color:unset}header nav.main .top .icon.icon-menu{display:block;position:absolute;top:1rem;right:1rem;width:35px;height:35px;z-index:999;transition:right 100ms ease}@media screen and (min-width: 600px){header nav.main .top .icon.icon-menu{display:none}}header nav.main .top .icon.icon-menu:before{content:"";display:block;background-image:url("/icons/menu-grid.svg");background-size:100%;position:absolute;width:35px;height:35px}header nav.main .top .icon.icon-menu:hover{cursor:pointer}header nav.main .top .icon.icon-menu.open{right:325px}header nav.main .top .icon.icon-menu.open:before{background-image:url("/icons/menu-close.svg")}header nav.main menu{position:absolute;right:0;top:0;width:0;background-color:#d7b94c;overflow:hidden;padding:0;margin:0;transition:width 100ms ease}@media(min-width: 600px){header nav.main menu{position:relative;top:unset;right:unset;width:100%;transition:unset;background-color:unset}}header nav.main menu.open{width:300px;box-shadow:0 0 5px 10px rgba(29,31,32,.45)}header nav.main menu li{padding:1rem 2rem}header nav.main menu li a{display:block;color:#fbfcff;text-decoration:none}header nav.main menu li a:hover{text-decoration:underline}.posts .list ul{list-style:none;padding:0}.posts .list ul li{padding:1rem 0}.posts .list ul li a{color:#fbfcff;text-decoration:none;display:block}.posts .list ul li a .post-title{margin:0}.photos{display:block}.photos .albums{display:flex;flex-direction:column;width:100%}@media(min-width: 800px){.photos .albums{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:auto}}.photos .albums{gap:1rem;list-style:none;padding:0}.photos .albums .album{display:block;width:100%}.photos .albums .album a{display:block;position:relative;color:#fff;text-decoration:none;overflow:hidden}.photos .albums .album a img{display:block;width:100%;height:auto}.photos .albums .album a .title,.photos .albums .album a .date,.photos .albums .album a .overlay{position:absolute;visibility:visible;margin:0}.photos .albums .album a .title,.photos .albums .album a .date,.photos .albums .album a .category{z-index:2}.photos .albums .album a .title{top:1rem;left:1rem}.photos .albums .album a .date{bottom:1rem;left:1rem;font-weight:bold}.photos .albums .album a .overlay{width:100%;height:auto;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,.333);z-index:1}.photos .albums .album a:hover .title,.photos .albums .album a:hover .date,.photos .albums .album a:hover .category,.photos .albums .album a:hover .overlay{visibility:hidden}.photos .albums .album a:hover img{transform:scale(1.01);transition:transform 100ms ease-in-out}.photos img{width:100%}#album .title{margin:0}#album .meta{border-bottom:5px solid #a6a695}#album .photos{display:flex;flex-direction:column;width:100%}@media(min-width: 800px){#album .photos{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:auto}}#album .photos{gap:1rem;list-style:none;padding:0}#album .photos .photo{width:100%;overflow:hidden}#album .photos .photo img{transition:transform 100ms ease-in-out}#album .photos .photo:hover{cursor:pointer}#album .photos .photo:hover img{transform:scale(1.01);transition:transform 100ms ease-in-out}#open-image{display:none;position:absolute;top:50%;left:0;transform:translateY(-50%);z-index:999}#open-image .close{cursor:pointer;font-size:2rem;position:absolute;right:0;top:0}#open-image img{width:100%;height:auto}#open-image #details{display:block;height:auto;padding:1rem 1rem;background-color:#a6a695}#open-image #details p{margin:0}#open-image #details #caption{font-size:1.5rem;padding-bottom:1rem}#open-image #details #location,#open-image #details #date,#open-image #details #equipment{font-size:1.25rem}#open-image.open{display:block}.posts{display:block}.posts h2,.posts h3{position:relative;margin:0;width:fit-content}.posts h2:after,.posts h3:after{content:"";display:block;width:75px;height:5px;background-color:#a6a695}.posts .list{padding-top:1rem;border-top:5px solid #d7b94c}.posts .list .post a .title{padding:.5rem 0}.posts .list .post a:hover{text-decoration:underline;text-decoration-color:#a6a695}.post{display:block}.post .title{margin:.5rem 0}.post .meta{padding-bottom:1rem;border-bottom:5px solid #a6a695}.post .meta .admin-actions{padding-bottom:1rem}.post .meta .published{font-size:1.125rem}.post .meta .category{font-size:1.125rem}.post .text p{font-size:1.25rem;line-height:1.5} +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:rgba(0,0,0,0)}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}@font-face{font-family:RobotoSlab-Regular;src:url("/fonts/RobotoSlab-Regular.ttf") format("opentype")}@font-face{font-family:RobotoSlab-Medium;src:url("/fonts/RobotoSlab-Regular.ttf") format("opentype")}@font-face{font-family:RobotoSlab-Bold;src:url("/fonts/RobotoSlab-Bold.ttf") format("opentype")}@font-face{font-family:RobotoSlab-Black;src:url("/fonts/RobotoSlab-Black.ttf") format("opentype")}@font-face{font-family:AtomicAge-Regular;src:url("/fonts/AtomicAge-Regular.ttf") format("opentype")}@font-face{font-family:Montserrat-Regular;src:url("/fonts/Montserrat-Regular.ttf") format("opentype")}@font-face{font-family:Montserrat-Light;src:url("/fonts/Montserrat-Light.ttf") format("opentype")}@font-face{font-family:Montserrat-Italic;src:url("/fonts/Montserrat-Italic.ttf") format("opentype")}@font-face{font-family:Montserrat-Bold;src:url("/fonts/Montserrat-Bold.ttf") format("opentype")}@font-face{font-family:Montserrat-SemiBold;src:url("/fonts/Montserrat-SemiBold.ttf") format("opentype")}@font-face{font-family:Orbitron-Regular;src:url("/fonts/Orbitron-Regular.ttf") format("opentype")}@font-face{font-family:Amarante;src:url("/fonts/Amarante-Regular.ttf") format("opentype")}h1,h2,h3,h4,h5,h6{font-family:Amarante;letter-spacing:2px}h1{font-size:2rem}@media screen and (min-width: 600px){h1{font-size:3rem}}h2{font-size:1.75rem}@media screen and (min-width: 600px){h2{font-size:2.5rem}}h3{font-size:1.5rem}@media screen and (min-width: 600px){h3{font-size:2rem}}h4{font-size:1.25rem}@media screen and (min-width: 600px){h4{font-size:1.5rem}}h5{font-size:1rem}@media screen and (min-width: 600px){h5{font-size:1.25rem}}h6{font-size:1rem}@media screen and (min-width: 600px){h6{font-size:1em}}p,a,li,span{font-family:RobotoSlab-Regular;letter-spacing:1px}strong{font-family:Montserrat-Bold}em{font-family:Montserrat-Italic}u{font-family:Montserrat-SemiBold}cite,q,small{font-family:Orbitron-Regular}s{font-family:Montserrat-SemiBold}h1,h2,h3,h4,h5,h6,p,a,span,time{color:#fbfcff}.secondary.layout--v1{display:block;position:relative;padding:.5rem .5rem;background-color:#d7b94c}.secondary.layout--v1 #root{display:flex;flex-direction:column}@media(min-width: 800px){.secondary.layout--v1 #root{display:grid;grid-template-columns:25% 75%;grid-template-rows:1fr;margin:0 auto;padding:1rem 1rem}}.secondary.layout--v1 #root{gap:1rem;position:relative;width:100%;height:100%;min-height:100vh;max-width:1200px;background-color:#1f2421;box-shadow:0 0 5px 10px rgba(29,31,32,.45)}.secondary.layout--v1 #root main{padding:1rem 1rem}.secondary.layout--v1.menu-open,.secondary.layout--v1.photo-open{overflow:hidden}.secondary.layout--v1.photo-open .overlay{display:block;position:absolute;top:0;left:0;width:100%;height:100%;background-color:#171a21;opacity:.9}header{display:block;position:sticky;overflow:auto;width:100%;height:50px;top:0;padding-bottom:1rem;background-color:#1f2421}@media(min-width: 600px){header{height:500px}}header.open{overflow:visible}header.open .top h1{display:none}header nav.main{display:block;position:relative}@media(min-width: 600px){header nav.main{display:flex;flex-direction:column;gap:2rem}}header nav.main .top{display:flex}header nav.main .top h1{margin:0}header nav.main .top h1 a{display:block;position:relative;top:1rem;left:1rem;font-size:1.5rem;text-decoration:none;color:#1d1f20;background-color:#d7b94c;border:2px solid #d7b94c;padding:2px 2px}header nav.main .top h1 a:hover{color:#d7b94c;background-color:unset}header nav.main .top .icon.icon-menu{display:block;position:absolute;top:1rem;right:1rem;width:35px;height:35px;z-index:999;transition:right 100ms ease}@media screen and (min-width: 600px){header nav.main .top .icon.icon-menu{display:none}}header nav.main .top .icon.icon-menu:before{content:"";display:block;background-image:url("/icons/menu-grid.svg");background-size:100%;position:absolute;width:35px;height:35px}header nav.main .top .icon.icon-menu:hover{cursor:pointer}header nav.main .top .icon.icon-menu.open{right:325px}header nav.main .top .icon.icon-menu.open:before{background-image:url("/icons/menu-close.svg")}header nav.main menu{position:absolute;right:0;top:0;width:0;background-color:#d7b94c;overflow:hidden;padding:0;margin:0;transition:width 100ms ease}@media(min-width: 600px){header nav.main menu{position:relative;top:unset;right:unset;width:100%;transition:unset;background-color:unset}}header nav.main menu.open{width:300px;box-shadow:0 0 5px 10px rgba(29,31,32,.45)}header nav.main menu li{padding:1rem 2rem}header nav.main menu li a{display:block;color:#fbfcff;text-decoration:none}header nav.main menu li a:hover{text-decoration:underline}.posts .list ul{list-style:none;padding:0}.posts .list ul li{padding:1rem 0}.posts .list ul li a{color:#fbfcff;text-decoration:none;display:block}.posts .list ul li a .post-title{margin:0}.photos{display:block}.photos .albums{display:flex;flex-direction:column;width:100%}@media(min-width: 800px){.photos .albums{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:auto}}.photos .albums{gap:1rem;list-style:none;padding:0}.photos .albums .album{display:block;width:100%}.photos .albums .album a{display:block;position:relative;color:#fff;text-decoration:none;overflow:hidden}.photos .albums .album a img{display:block;width:100%;height:auto}.photos .albums .album a .title,.photos .albums .album a .date,.photos .albums .album a .overlay{position:absolute;visibility:visible;margin:0}.photos .albums .album a .title,.photos .albums .album a .date,.photos .albums .album a .category{z-index:2}.photos .albums .album a .title{top:1rem;left:1rem}.photos .albums .album a .date{bottom:1rem;left:1rem;font-weight:bold}.photos .albums .album a .overlay{width:100%;height:auto;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,.333);z-index:1}.photos .albums .album a:hover .title,.photos .albums .album a:hover .date,.photos .albums .album a:hover .category,.photos .albums .album a:hover .overlay{visibility:hidden}.photos .albums .album a:hover img{transform:scale(1.01);transition:transform 100ms ease-in-out}.photos img{width:100%}#album .title{margin:0}#album .meta{border-bottom:5px solid #a6a695}#album .photos{display:flex;flex-direction:column;width:100%}@media(min-width: 800px){#album .photos{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:auto}}#album .photos{gap:1rem;list-style:none;padding:0}#album .photos .photo{width:100%;overflow:hidden}#album .photos .photo img{transition:transform 100ms ease-in-out}#album .photos .photo:hover{cursor:pointer}#album .photos .photo:hover img{transform:scale(1.01);transition:transform 100ms ease-in-out}#open-image{display:none;position:absolute;top:50%;left:0;transform:translateY(-50%);z-index:999}#open-image .close{cursor:pointer;font-size:2rem;position:absolute;right:0;top:0}#open-image img{width:100%;height:auto}#open-image #details{display:block;height:auto;padding:1rem 1rem;background-color:#a6a695}#open-image #details p{margin:0}#open-image #details #caption{font-size:1.5rem;padding-bottom:1rem}#open-image #details #location,#open-image #details #date,#open-image #details #equipment{font-size:1.25rem}#open-image.open{display:block}.posts{display:block}.posts h2,.posts h3{position:relative;margin:0;width:fit-content}.posts h2:after,.posts h3:after{content:"";display:block;width:75px;height:5px;background-color:#a6a695}.posts .list{padding-top:1rem;border-top:5px solid #d7b94c}.posts .list .post a .title{padding:.5rem 0}.posts .list .post a:hover{text-decoration:underline;text-decoration-color:#a6a695}.post{display:block}.post .title{margin:.5rem 0}.post .meta{padding-bottom:1rem;border-bottom:5px solid #a6a695}.post .meta .admin-actions{padding-bottom:1rem}.post .meta .published{font-size:1.125rem}.post .meta .category{font-size:1.125rem}.post .text p{font-size:1.25rem;line-height:1.5}.post .tags{margin-top:2rem;background-color:dimgray}.post .tags .list{display:flex;align-items:center;gap:1rem;margin:0;list-style:none;padding:.5rem .5rem}.post .tags .list p{font-size:.85rem}.post .tags .list .tag a{font-size:.85rem} diff --git a/assets/styles/scss/front/_posts.scss b/assets/styles/scss/front/_posts.scss index a4a214f..4c0cb2a 100644 --- a/assets/styles/scss/front/_posts.scss +++ b/assets/styles/scss/front/_posts.scss @@ -70,4 +70,28 @@ font-size: 1.25rem; line-height: 1.5; } + + .tags { + margin-top: 2rem; + background-color: colors.$primaryGrey; + + .list { + display: flex; + align-items: center; + gap: 1rem; + margin: 0; + list-style: none; + padding: 0.5rem 0.5rem; + + p { + font-size: 0.85rem; + } + + .tag { + a { + font-size: 0.85rem; + } + } + } + } } \ No newline at end of file diff --git a/migrations/Version20260608193439.php b/migrations/Version20260608193439.php new file mode 100644 index 0000000..5e10f48 --- /dev/null +++ b/migrations/Version20260608193439.php @@ -0,0 +1,37 @@ +addSql('CREATE TABLE photos_tag (photos_id INT NOT NULL, tag_id INT NOT NULL, PRIMARY KEY (photos_id, tag_id))'); + $this->addSql('CREATE INDEX IDX_CBE2DE88301EC62 ON photos_tag (photos_id)'); + $this->addSql('CREATE INDEX IDX_CBE2DE88BAD26311 ON photos_tag (tag_id)'); + $this->addSql('ALTER TABLE photos_tag ADD CONSTRAINT FK_CBE2DE88301EC62 FOREIGN KEY (photos_id) REFERENCES photos (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE photos_tag ADD CONSTRAINT FK_CBE2DE88BAD26311 FOREIGN KEY (tag_id) REFERENCES tag (id) ON DELETE CASCADE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE photos_tag DROP CONSTRAINT FK_CBE2DE88301EC62'); + $this->addSql('ALTER TABLE photos_tag DROP CONSTRAINT FK_CBE2DE88BAD26311'); + $this->addSql('DROP TABLE photos_tag'); + } +} diff --git a/src/Controller/Brain/BrainCategoryController.php b/src/Controller/Brain/BrainCategoryController.php new file mode 100644 index 0000000..57c23ec --- /dev/null +++ b/src/Controller/Brain/BrainCategoryController.php @@ -0,0 +1,55 @@ +getRepository(Category::class)->findAll(); + return $this->render('brain/taxonomy/index.html.twig', [ + 'taxonomy' => $categories, + 'type' => 'Category' + ]); + } + + #[Route('/brain/category/new', name: 'brain_categories_new')] + public function new(EntityManagerInterface $entityManager, Request $request): Response + { + + $form = $this->createForm(CategoryType::class); + + $form->handleRequest($request); + + if ($form->isSubmitted()) { + $data = $form->getData(); + $category = new Category(); + + $category->setTitle($data->getTitle()); + + $entityManager->persist($category); + + $entityManager->flush(); + + return $this->redirectToRoute('brain_categories_list'); + } + + return $this->render('brain/taxonomy/create.html.twig', [ + 'action' => 'New', + 'form' => $form, + 'type' => 'Category' + ]); + } +} diff --git a/src/Controller/Brain/BrainPhotosController.php b/src/Controller/Brain/BrainPhotosController.php index e9dd3db..ef95400 100644 --- a/src/Controller/Brain/BrainPhotosController.php +++ b/src/Controller/Brain/BrainPhotosController.php @@ -43,6 +43,10 @@ final class BrainPhotosController extends AbstractController $photos->setText($data->getText()); $photos->setUrl($data->getUrl()); + foreach ($data->getTags() as $tag) { + $photos->addTag($tag); + } + $tax = $request->request->all('photos'); if ($data->getCategory() == null) { diff --git a/src/Controller/Brain/BrainPostController.php b/src/Controller/Brain/BrainPostController.php index 718316a..529e1f7 100644 --- a/src/Controller/Brain/BrainPostController.php +++ b/src/Controller/Brain/BrainPostController.php @@ -53,6 +53,10 @@ final class BrainPostController extends AbstractController } else { $post->SetPublished(false); } + + foreach ($data->getTags() as $tag) { + $post->addTag($tag); + } $tax = $request->request->all('post'); diff --git a/src/Controller/Brain/BrainTagController.php b/src/Controller/Brain/BrainTagController.php new file mode 100644 index 0000000..6828cfd --- /dev/null +++ b/src/Controller/Brain/BrainTagController.php @@ -0,0 +1,56 @@ +getRepository(Tag::class)->findAll(); + + return $this->render('brain/taxonomy/index.html.twig', [ + 'taxonomy' => $tags, + 'type' => 'Tag' + ]); + } + + #[Route('/brain/tag/new', name: 'brain_tags_new')] + public function new(EntityManagerInterface $entityManager, Request $request): Response + { + + $form = $this->createForm(TagType::class); + + $form->handleRequest($request); + + if ($form->isSubmitted()) { + $data = $form->getData(); + $Tag = new Tag(); + + $Tag->setTitle($data->getTitle()); + + $entityManager->persist($Tag); + + $entityManager->flush(); + + return $this->redirectToRoute('brain_tags_list'); + } + + return $this->render('brain/taxonomy/create.html.twig', [ + 'action' => 'New', + 'form' => $form, + 'type' => 'Tag' + ]); + } +} diff --git a/src/Controller/FrontEnd/TagController.php b/src/Controller/FrontEnd/TagController.php new file mode 100644 index 0000000..1db4870 --- /dev/null +++ b/src/Controller/FrontEnd/TagController.php @@ -0,0 +1,48 @@ +getRepository(Tag::class)->findAll(); + + $tagsDisplay = []; + + foreach ($tags as $index => $tag) { + $tagsDisplay[] = [ + 'title' => $tag->getTitle(), + 'urlSafeTitle' => strtolower(str_replace(' ', '-', $tag->getTitle())), + 'count' => $tag->getCount(), + ]; + } + + return $this->render('front/tag/index.html.twig', [ + 'tags' => $tagsDisplay + ]); + } + + #[Route('/tags/{tagTitle}', name: 'front_end_tag_detail')] + public function detail(EntityManagerInterface $entityManager, string $tagTitle): Response + { + $formattedTitle = ucwords(str_replace('-', ' ', $tagTitle)); + $tag = $entityManager->getRepository(tag::class)->findOneBy(['title' => $formattedTitle]); + + return $this->render('front/tag/detail.html.twig', [ + 'title' => $tag->getTitle(), + 'posts' => $tag->getPosts(), + 'photos' => $tag->getPhotos(), + 'count' => $tag->getCount() + ]); + } +} diff --git a/src/Entity/Photos.php b/src/Entity/Photos.php index eb59b64..aa03c86 100644 --- a/src/Entity/Photos.php +++ b/src/Entity/Photos.php @@ -40,9 +40,16 @@ class Photos #[ORM\Column(length: 255)] private ?string $thumbnail = null; + /** + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'photos')] + private Collection $tags; + public function __construct() { $this->uploads = new ArrayCollection(); + $this->tags = new ArrayCollection(); } public function getId(): ?int @@ -151,4 +158,28 @@ class Photos return $this; } + + /** + * @return Collection + */ + public function getTags(): Collection + { + return $this->tags; + } + + public function addTag(Tag $tag): static + { + if (!$this->tags->contains($tag)) { + $this->tags->add($tag); + } + + return $this; + } + + public function removeTag(Tag $tag): static + { + $this->tags->removeElement($tag); + + return $this; + } } diff --git a/src/Entity/Tag.php b/src/Entity/Tag.php index b5420a9..5fda96f 100644 --- a/src/Entity/Tag.php +++ b/src/Entity/Tag.php @@ -24,9 +24,16 @@ class Tag #[ORM\ManyToMany(targetEntity: Post::class, mappedBy: 'tags')] private Collection $posts; + /** + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: Photos::class, mappedBy: 'tags')] + private Collection $photos; + public function __construct() { $this->posts = new ArrayCollection(); + $this->photos = new ArrayCollection(); } public function getId(): ?int @@ -72,4 +79,46 @@ class Tag return $this; } + + /** + * @return Collection + */ + public function getPhotos(): Collection + { + return $this->photos; + } + + public function addPhoto(Photos $photo): static + { + if (!$this->photos->contains($photo)) { + $this->photos->add($photo); + $photo->addTag($this); + } + + return $this; + } + + public function removePhoto(Photos $photo): static + { + if ($this->photos->removeElement($photo)) { + $photo->removeTag($this); + } + + return $this; + } + + public function getCount() : int + { + return $this->posts->count() + $this->photos->count(); + } + + public function getUrlSafeTitle() : string + { + return strtolower(str_replace(' ', '-', $this->title)); + } + + public function getDisplaySafeTitle(string $urlSafeTitle) : string + { + return ucwords(str_replace('-', ' ', $urlSafeTitle)); + } } diff --git a/src/Form/CategoryType.php b/src/Form/CategoryType.php new file mode 100644 index 0000000..6b71a9c --- /dev/null +++ b/src/Form/CategoryType.php @@ -0,0 +1,26 @@ +add('title') + ->add('save', SubmitType::class); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Category::class, + ]); + } +} diff --git a/src/Form/PhotosType.php b/src/Form/PhotosType.php index 77f83b1..f377131 100644 --- a/src/Form/PhotosType.php +++ b/src/Form/PhotosType.php @@ -5,6 +5,7 @@ namespace App\Form; use App\Entity\Photos; use App\Form\CategoryAutocompleteField; +use App\Form\TagAutocompleteField; use App\Form\PhotoType; use App\Form\DataTransformer\UploadDataTransformer; @@ -25,6 +26,7 @@ class PhotosType extends AbstractType ->add('title') ->add('date', DateType::class) ->add('category', CategoryAutocompleteField::class) + ->add('tags', TagAutocompleteField::class, ['required' => false]) ->add('text') ->add('thumbnail', FileType::class, [ 'label' => 'Thumbnail', diff --git a/src/Form/PostType.php b/src/Form/PostType.php index 4d7a923..9a283b5 100644 --- a/src/Form/PostType.php +++ b/src/Form/PostType.php @@ -8,15 +8,11 @@ use App\Form\CategoryAutocompleteField; use App\Form\TagAutocompleteField; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\Type\ChoiceType; -use Symfony\Component\Form\Extension\Core\Type\CollectionType; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Form\FormEvent; -use Symfony\Component\Form\FormEvents; use Symfony\Component\OptionsResolver\OptionsResolver; class PostType extends AbstractType @@ -28,7 +24,7 @@ class PostType extends AbstractType ->add('date', DateType::class) ->add('text', TextareaType::class) ->add('category', CategoryAutocompleteField::class) - //->add('tags', TagAutocompleteField::class, ['required' => false]) + ->add('tags', TagAutocompleteField::class, ['required' => false]) ->add('url', TextType::class) ->add('published') ->add('save', SubmitType::class, ['label' => 'Save']) diff --git a/src/Form/TagAutocompleteField.php b/src/Form/TagAutocompleteField.php index 627deff..7df8900 100644 --- a/src/Form/TagAutocompleteField.php +++ b/src/Form/TagAutocompleteField.php @@ -23,12 +23,6 @@ class TagAutocompleteField extends AbstractType 'choice_label' => 'title', 'multiple' => true, 'required' => false, - /* TODO this isn't natively supported collections/choice type - so we're going to manually do it for now - */ - /*'tom_select_options' => [ - 'create' => true, - ] */ ]); } diff --git a/src/Form/TagType.php b/src/Form/TagType.php new file mode 100644 index 0000000..d396164 --- /dev/null +++ b/src/Form/TagType.php @@ -0,0 +1,27 @@ +add('title') + ->add('save', SubmitType::class); + + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Tag::class, + ]); + } +} diff --git a/templates/brain/menu/menu.html.twig b/templates/brain/menu/menu.html.twig index 4d5e28a..db1da1b 100644 --- a/templates/brain/menu/menu.html.twig +++ b/templates/brain/menu/menu.html.twig @@ -23,7 +23,9 @@ diff --git a/templates/brain/photos/create.html.twig b/templates/brain/photos/create.html.twig index f20baab..929251f 100644 --- a/templates/brain/photos/create.html.twig +++ b/templates/brain/photos/create.html.twig @@ -20,6 +20,9 @@
{{ form_row(form.category) }}
+
+ {{ form_row(form.tags) }} +
{{ form_row(form.thumbnail) }}
diff --git a/templates/brain/post/create.html.twig b/templates/brain/post/create.html.twig index 2a1498c..fe7daa0 100644 --- a/templates/brain/post/create.html.twig +++ b/templates/brain/post/create.html.twig @@ -25,9 +25,9 @@
{{ form_row(form.category) }}
- {#
+
{{ form_row(form.tags) }} -
#} +
{{ form_row(form.url) }}
diff --git a/templates/brain/taxonomy/create.html.twig b/templates/brain/taxonomy/create.html.twig new file mode 100644 index 0000000..700a111 --- /dev/null +++ b/templates/brain/taxonomy/create.html.twig @@ -0,0 +1,23 @@ +{% extends 'brain/base.html.twig' %} + +{% block title %}{{ action|capitalize }} {{ type }} {% endblock %} + +{% block page_title %}

Add New {{ type }}

{% endblock %} + +{# {% block actions%}Actions here{% endblock %} #} + +{% block admin %} +
+ {{ form_start(form) }} +
+ {{ form_errors(form) }} +
+
+ {{ form_row(form.title) }} +
+
+ {{ form_rest(form) }} +
+ {{ form_end(form) }} +
+{% endblock %} diff --git a/templates/brain/taxonomy/index.html.twig b/templates/brain/taxonomy/index.html.twig new file mode 100644 index 0000000..c13130b --- /dev/null +++ b/templates/brain/taxonomy/index.html.twig @@ -0,0 +1,27 @@ +{% extends 'brain/base.html.twig' %} + +{% block title %}{{ type }} {% endblock %} + +{% block page_title %}

All {{ type }}

{% endblock %} + +{# {% block actions%}Actions here{% endblock %} #} + +{% block admin %} +
+

New {{ type }}

+ + + + + + + + {% for term in taxonomy %} + + + + {% endfor %} + +
Title
{{term.title}}
+
+{% endblock %} diff --git a/templates/front/home/index.html.twig b/templates/front/home/index.html.twig index e0aefcf..731fdc7 100644 --- a/templates/front/home/index.html.twig +++ b/templates/front/home/index.html.twig @@ -1,6 +1,6 @@ {% extends 'home.html.twig' %} -{% block title %}Hello | Alex Daniels{% endblock %} +{% block title %}Alex Daniels{% endblock %} {% block nav %} diff --git a/templates/front/post/detail.html.twig b/templates/front/post/detail.html.twig index bdd664a..4504ab9 100644 --- a/templates/front/post/detail.html.twig +++ b/templates/front/post/detail.html.twig @@ -17,5 +17,13 @@
{{ post.text|raw }}
+
+
    +

    More like this:

    + {% for tag in post.tags %} +
  • {{ tag.title }}
  • + {% endfor %} +
{% endblock %} diff --git a/templates/front/tag/detail.html.twig b/templates/front/tag/detail.html.twig new file mode 100644 index 0000000..c33c1f5 --- /dev/null +++ b/templates/front/tag/detail.html.twig @@ -0,0 +1,28 @@ +{% extends 'base.html.twig' %} + +{% block title %}Tags | Alex Daniels{% endblock %} + +{% block body %} +
+ {% if posts|length %} +
+

Posts

+ +
+ {% endif %} + {% if photos|length %} +
+

Photos

+ +
+ {% endif %} +
+{% endblock %} diff --git a/templates/front/tag/index.html.twig b/templates/front/tag/index.html.twig new file mode 100644 index 0000000..4ad9621 --- /dev/null +++ b/templates/front/tag/index.html.twig @@ -0,0 +1,16 @@ +{% extends 'base.html.twig' %} + +{% block title %}Tags | Alex Daniels{% endblock %} + +{% block body %} +
+

Tags

+ +
+{% endblock %}