diff --git a/.gitignore b/.gitignore index 55e5d8a..734d703 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +local.db # Created by https://www.toptal.com/developers/gitignore/api/go # Edit at https://www.toptal.com/developers/gitignore?templates=go diff --git a/assets/styles.css b/assets/styles.css index a136c83..47735b6 100644 --- a/assets/styles.css +++ b/assets/styles.css @@ -1 +1 @@ -/*! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.static{position:static}.absolute{position:absolute}.relative{position:relative}.inset-y-0{top:0;bottom:0}.right-0{right:0}.mx-auto{margin-left:auto;margin-right:auto}.mb-2{margin-bottom:.5rem}.mt-10{margin-top:2.5rem}.block{display:block}.flex{display:flex}.table{display:table}.hidden{display:none}.h-16{height:4rem}.h-8{height:2rem}.w-auto{width:auto}.w-full{width:100%}.max-w-7xl{max-width:80rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-3{gap:.75rem}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.overflow-x-auto{overflow-x:auto}.whitespace-nowrap{white-space:nowrap}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.p-6{padding:1.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.pr-2{padding-right:.5rem}.text-left{text-align:left}.text-2xl{font-size:1.5rem;line-height:2rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-light{font-weight:300}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.tracking-tight{letter-spacing:-.025em}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}@media (min-width:640px){.sm\:static{position:static}.sm\:inset-auto{inset:auto}.sm\:ml-6{margin-left:1.5rem}.sm\:block{display:block}.sm\:items-stretch{align-items:stretch}.sm\:justify-start{justify-content:flex-start}.sm\:pr-0{padding-right:0}}.rtl\:text-right:where([dir=rtl],[dir=rtl] *){text-align:right}@media (prefers-color-scheme:dark){.dark\:border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.dark\:bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.dark\:bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.dark\:text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.dark\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.dark\:hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}} \ No newline at end of file +/*! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.static{position:static}.absolute{position:absolute}.relative{position:relative}.inset-y-0{top:0;bottom:0}.right-0{right:0}.mx-auto{margin-left:auto;margin-right:auto}.mb-2{margin-bottom:.5rem}.mt-10{margin-top:2.5rem}.block{display:block}.flex{display:flex}.table{display:table}.hidden{display:none}.h-16{height:4rem}.h-8{height:2rem}.w-auto{width:auto}.w-full{width:100%}.max-w-7xl{max-width:80rem}.max-w-96,.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-3{gap:.75rem}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.overflow-x-auto{overflow-x:auto}.whitespace-nowrap{white-space:nowrap}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.bg-blue-700{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-red-600{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity))}.bg-red-400{--tw-bg-opacity:1;background-color:rgb(248 113 113/var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity))}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-5{padding:1.25rem}.p-3{padding:.75rem}.p-1{padding:.25rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.pr-2{padding-right:.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-light{font-weight:300}.font-medium{font-weight:500}.font-normal{font-weight:400}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.tracking-tight{letter-spacing:-.025em}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\:bg-blue-800:hover{--tw-bg-opacity:1;background-color:rgb(30 64 175/var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.focus\:border-blue-500:focus{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity))}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:ring-4:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-blue-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(147 197 253/var(--tw-ring-opacity))}.focus\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(59 130 246/var(--tw-ring-opacity))}@media (min-width:640px){.sm\:static{position:static}.sm\:inset-auto{inset:auto}.sm\:ml-6{margin-left:1.5rem}.sm\:block{display:block}.sm\:items-stretch{align-items:stretch}.sm\:justify-start{justify-content:flex-start}.sm\:pr-0{padding-right:0}}.rtl\:text-right:where([dir=rtl],[dir=rtl] *){text-align:right}@media (prefers-color-scheme:dark){.dark\:border-gray-500{--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}.dark\:border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.dark\:bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity))}.dark\:bg-gray-600{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}.dark\:bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.dark\:bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.dark\:text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.dark\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.dark\:placeholder-gray-400::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175/var(--tw-placeholder-opacity))}.dark\:placeholder-gray-400::placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175/var(--tw-placeholder-opacity))}.dark\:hover\:bg-blue-700:hover{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity))}.dark\:hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.dark\:focus\:ring-blue-800:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(30 64 175/var(--tw-ring-opacity))}} \ No newline at end of file diff --git a/go.mod b/go.mod index 9b3c907..0139fc1 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,35 @@ go 1.22.2 require github.com/a-h/templ v0.2.707 -require github.com/mergestat/timediff v0.0.3 // indirect +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/mergestat/timediff v0.0.3 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + golang.org/x/sys v0.19.0 // indirect + modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b // indirect + modernc.org/libc v1.49.0 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.7.2 // indirect + modernc.org/sqlite v1.29.5 // indirect + modernc.org/strutil v1.2.0 // indirect + modernc.org/token v1.1.0 // indirect +) require ( github.com/go-chi/chi v1.5.5 github.com/go-chi/chi/v5 v5.0.12 // indirect github.com/gorcon/rcon v1.3.5 // indirect github.com/joho/godotenv v1.5.1 // indirect + github.com/uptrace/bun v1.2.1 + github.com/uptrace/bun/dialect/sqlitedialect v1.2.1 + github.com/uptrace/bun/driver/sqliteshim v1.2.1 ) diff --git a/go.sum b/go.sum index c67a7bc..c07bbdc 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,57 @@ github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U= github.com/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE= github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw= github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorcon/rcon v1.3.5 h1:YE/Vrw6R99uEP08wp0EjdPAP3Jwz/ys3J8qxI1nYoeU= github.com/gorcon/rcon v1.3.5/go.mod h1:zR1qfKZttF8vAgH1NsP6CdpachOvLDq8jE64NboTpIM= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mergestat/timediff v0.0.3 h1:ucCNh4/ZrTPjFZ081PccNbhx9spymCJkFxSzgVuPU+Y= github.com/mergestat/timediff v0.0.3/go.mod h1:yvMUaRu2oetc+9IbPLYBJviz6sA7xz8OXMDfhBl7YSI= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= +github.com/uptrace/bun v1.2.1 h1:2ENAcfeCfaY5+2e7z5pXrzFKy3vS8VXvkCag6N2Yzfk= +github.com/uptrace/bun v1.2.1/go.mod h1:cNg+pWBUMmJ8rHnETgf65CEvn3aIKErrwOD6IA8e+Ec= +github.com/uptrace/bun/dialect/sqlitedialect v1.2.1 h1:IprvkIKUjEjvt4VKpcmLpbMIucjrsmUPJOSlg19+a0Q= +github.com/uptrace/bun/dialect/sqlitedialect v1.2.1/go.mod h1:mMQf4NUpgY8bnOanxGmxNiHCdALOggS4cZ3v63a9D/o= +github.com/uptrace/bun/driver/sqliteshim v1.2.1 h1:xBsGsoMIskK7+dhtWIQ4CrO+UTWzC96G3vGzNDkr5aQ= +github.com/uptrace/bun/driver/sqliteshim v1.2.1/go.mod h1:oJtOPSCDdDHgNw/0jwIGr+V0yUFxQ8NrBwJ3xbp4XOU= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b h1:BnN1t+pb1cy61zbvSUV7SeI0PwosMhlAEi/vBY4qxp8= +modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/libc v1.49.0 h1:/kkNBuCXvlTbOGwrQdgR67eK1Y9+kR+fhdBd89C64VM= +modernc.org/libc v1.49.0/go.mod h1:DNz0lgQgT6FPIPm8rHtjFj0FL5/YOr/NYFXWYBcSxMw= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= +modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= +modernc.org/sqlite v1.29.5 h1:8l/SQKAjDtZFo9lkJLdk8g9JEOeYRG4/ghStDCCTiTE= +modernc.org/sqlite v1.29.5/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/main.go b/main.go index c56e562..5a4ce30 100644 --- a/main.go +++ b/main.go @@ -6,10 +6,10 @@ import ( "log/slog" "net/http" "os" - "time" "gitea.henriburau.de/haw-lan/cod4watcher/models" "gitea.henriburau.de/haw-lan/cod4watcher/routes" + "gitea.henriburau.de/haw-lan/cod4watcher/services" "github.com/go-chi/chi" "github.com/joho/godotenv" ) @@ -19,25 +19,21 @@ func main() { log.Fatal(err) } - cod4server, err := models.NewCOD4ServerStatus("80.57.28.137", "28960", 1*time.Second) - if err != nil { - log.Panic(err) - } - - status, err := cod4server.GetServerStatus() - if err != nil { - log.Panic(err) - } - - log.Printf("%+v", status) - router := chi.NewMux() - server := &routes.Server{} + persistence, err := models.NewSQLitePersistence("local.db") + if err != nil { + log.Fatal(err) + } + + cs := services.NewCaptureService(persistence) + server := routes.NewServer(cs) router.Handle("/*", public()) router.Get("/health", routes.Make(server.HandleHealth)) router.Get("/captures/{captureID}", routes.Make(server.HandleCapture)) + router.Get("/new/capture", routes.Make(server.HandleCaptureForm)) + router.Post("/new/capture", routes.Make(server.HandleCaptureCreate)) router.Get("/", routes.Make(server.HandleHome)) listenAddr := os.Getenv("LISTEN_ADDR") diff --git a/models/capture.go b/models/capture.go index 8c5147a..a30db4b 100644 --- a/models/capture.go +++ b/models/capture.go @@ -11,27 +11,29 @@ import ( type MapScoreList []MapScore type Capture struct { - Id uint + ID int64 `bun:",pk,autoincrement"` Host string Port string Name string Active bool Start time.Time - MapScores MapScoreList + MapScores MapScoreList `bun:"rel:has-many,join:id=capture_id"` } type MapScore struct { - Id uint + ID int64 `bun:",pk,autoincrement"` + CaptureID int64 `bun:"capture_id"` StartTime time.Time Map string - ScoreList []Score + ScoreList []Score `bun:"rel:has-many,join:id=mapscore_id"` } type Score struct { - Id uint - Name string - Score int - Ping int + ID int64 `bun:",pk,autoincrement"` + MapScoreID int64 `bun:"mapscore_id"` + Name string + Score int + Ping int } type ResultTable struct { diff --git a/models/cod4server.go b/models/cod4server.go index a2a9b4e..1c70052 100644 --- a/models/cod4server.go +++ b/models/cod4server.go @@ -12,7 +12,7 @@ import ( "time" ) -var timeLayout = "Mon Jun 2 15:04:05 2006" +var timeLayout = "Mon Jan 2 15:04:05 2006" type CoD4Server struct { server string @@ -109,7 +109,7 @@ func parseServerData(data string) (*CoD4ServerStatus, error) { c.serverData["_Maps"] = strings.Join(strings.Split(c.serverData["_Maps"], "-"), ",") c.MapName = c.serverData["mapname"] - startTime, err := time.Parse(timeLayout, c.serverData["g_mapStartTime"]) + startTime, err := time.ParseInLocation(timeLayout, c.serverData["g_mapStartTime"], time.Local) if err != nil { return nil, err } diff --git a/models/persistence.go b/models/persistence.go index f323ee8..806f3c7 100644 --- a/models/persistence.go +++ b/models/persistence.go @@ -1,4 +1,58 @@ package models +import ( + "context" + "database/sql" + "fmt" + + "github.com/uptrace/bun" + "github.com/uptrace/bun/dialect/sqlitedialect" + "github.com/uptrace/bun/driver/sqliteshim" +) + type Persistence interface { + CreateCapture(context.Context, *Capture) error + GetCaptures(context.Context) ([]Capture, error) +} + +type SQLitePersistence struct { + db *bun.DB +} + +func NewSQLitePersistence(dbloc string) (*SQLitePersistence, error) { + sqldb, err := sql.Open(sqliteshim.ShimName, fmt.Sprintf("file:%s", dbloc)) + if err != nil { + return nil, err + } + + db := bun.NewDB(sqldb, sqlitedialect.New()) + + ctx := context.Background() + + for _, model := range []interface{}{(*Capture)(nil), (*MapScore)(nil), (*Score)(nil)} { + _, err := db.NewCreateTable().Model(model).IfNotExists().Exec(ctx) + if err != nil { + return nil, err + } + } + + return &SQLitePersistence{ + db, + }, nil +} + +func (s *SQLitePersistence) GetCaptures(ctx context.Context) ([]Capture, error) { + var captures []Capture + err := s.db.NewSelect().Model(&captures).Scan(ctx) + if err != nil { + return nil, err + } + + return captures, nil +} + +func (s *SQLitePersistence) CreateCapture(ctx context.Context, capture *Capture) error { + _, err := s.db.NewInsert().Model(capture).Exec(ctx) + + return err } diff --git a/routes/capture.go b/routes/capture.go index 10e57d5..fa7b404 100644 --- a/routes/capture.go +++ b/routes/capture.go @@ -4,6 +4,7 @@ import ( "net/http" "strconv" + "gitea.henriburau.de/haw-lan/cod4watcher/models" "gitea.henriburau.de/haw-lan/cod4watcher/views/capture" "github.com/go-chi/chi" ) @@ -22,3 +23,54 @@ func (s *Server) HandleCapture(w http.ResponseWriter, r *http.Request) error { return Render(w, r, capture.Capture(foundCapture)) } + +func (s *Server) HandleCaptureForm(w http.ResponseWriter, r *http.Request) error { + return Render(w, r, capture.CaptureForm(capture.CaptureFormValues{}, map[string]string{})) +} + +func (s *Server) HandleCaptureCreate(w http.ResponseWriter, r *http.Request) error { + formValues, errors := parseCaptureFormAndValidate(r) + if len(errors) > 0 { + return Render(w, r, capture.CaptureForm(formValues, errors)) + } + + capture := &models.Capture{ + Host: formValues.Host, + Port: formValues.Port, + Name: formValues.Name, + Active: true, + } + + err := s.cs.CreateCapture(r.Context(), capture) + if err != nil { + return err + } + + return hxRedirect(w, r, "/") +} + +func parseCaptureFormAndValidate(r *http.Request) (capture.CaptureFormValues, map[string]string) { + r.ParseForm() + + formValues := capture.CaptureFormValues{ + Host: r.FormValue("host"), + Port: r.FormValue("port"), + Name: r.FormValue("name"), + } + + errors := map[string]string{} + + if len(formValues.Host) <= 0 { + errors["host"] = "Host needs to be set" + } + + if len(formValues.Port) <= 0 { + errors["port"] = "Port needs to be set" + } + + if len(formValues.Name) < 5 || len(formValues.Name) > 50 { + errors["name"] = "Name needs to be between 5 and 50 characters long" + } + + return formValues, errors +} diff --git a/routes/home.go b/routes/home.go index ec90166..055351d 100644 --- a/routes/home.go +++ b/routes/home.go @@ -7,7 +7,7 @@ import ( ) func (s *Server) HandleHome(w http.ResponseWriter, r *http.Request) error { - captureList, err := s.cs.GetActiveCapures() + captureList, err := s.cs.GetActiveCapures(r.Context()) if err != nil { return err } diff --git a/routes/routes.go b/routes/routes.go index 52b1cf2..7a38687 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -22,6 +22,23 @@ type Server struct { cs *services.CaptureService } +func NewServer(cs *services.CaptureService) *Server { + return &Server{ + cs, + } +} + func Render(w http.ResponseWriter, r *http.Request, c templ.Component) error { return c.Render(r.Context(), w) } + +func hxRedirect(w http.ResponseWriter, r *http.Request, url string) error { + if len(r.Header.Get("HX-Request")) > 0 { + w.Header().Set("hx-redirect", url) + w.WriteHeader(http.StatusSeeOther) + return nil + } + + http.Redirect(w, r, url, http.StatusSeeOther) + return nil +} diff --git a/services/capture.go b/services/capture.go index f5d0575..9f691bc 100644 --- a/services/capture.go +++ b/services/capture.go @@ -1,26 +1,49 @@ package services import ( + "context" "time" "gitea.henriburau.de/haw-lan/cod4watcher/models" ) type CaptureService struct { + p models.Persistence } -var captures = []models.Capture{ - {Id: 1, Host: "80.57.28.137", Port: "28960", Active: true, Name: "Gungame HAW-LAN 11", Start: time.Now().Add(-1 * time.Hour)}, - {Id: 1, Host: "80.57.28.137", Port: "28960", Active: true, Name: "Gungame HAW-LAN 12", Start: time.Now().Add(-5 * time.Minute)}, +var mockCaptures = []models.Capture{ + {ID: 1, Host: "80.57.28.137", Port: "28960", Active: true, Name: "Gungame HAW-LAN 11", Start: time.Now().Add(-1 * time.Hour)}, + {ID: 1, Host: "80.57.28.137", Port: "28960", Active: true, Name: "Gungame HAW-LAN 12", Start: time.Now().Add(-5 * time.Minute)}, } -func (cs *CaptureService) GetActiveCapures() ([]models.Capture, error) { +func NewCaptureService(persistence models.Persistence) *CaptureService { + return &CaptureService{ + p: persistence, + } +} - return captures, nil +func (cs *CaptureService) CreateCapture(ctx context.Context, capture *models.Capture) error { + return cs.p.CreateCapture(ctx, capture) +} + +func (cs *CaptureService) GetActiveCapures(ctx context.Context) ([]models.Capture, error) { + captures, err := cs.p.GetCaptures(ctx) + if err != nil { + return nil, err + } + + var result []models.Capture + for _, capture := range captures { + if capture.Active { + result = append(result, capture) + } + } + + return result, nil } func (cs *CaptureService) GetCaptureById(id int) (*models.Capture, error) { - capture := captures[0] + capture := mockCaptures[0] server, err := models.NewCOD4ServerStatus(capture.Host, capture.Port, time.Second) if err != nil { return nil, err @@ -33,7 +56,7 @@ func (cs *CaptureService) GetCaptureById(id int) (*models.Capture, error) { capture.MapScores = []models.MapScore{ { - Id: 0, + ID: 0, StartTime: status.MapStartTime, Map: status.MapName, ScoreList: status.Score, diff --git a/tmp/bin/main b/tmp/bin/main deleted file mode 100755 index 175e0f9..0000000 Binary files a/tmp/bin/main and /dev/null differ diff --git a/tmp/build-errors.log b/tmp/build-errors.log deleted file mode 100644 index e0022ad..0000000 --- a/tmp/build-errors.log +++ /dev/null @@ -1 +0,0 @@ -exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file diff --git a/views/capture/capture_form.templ b/views/capture/capture_form.templ new file mode 100644 index 0000000..207e329 --- /dev/null +++ b/views/capture/capture_form.templ @@ -0,0 +1,50 @@ +package capture + +import "gitea.henriburau.de/haw-lan/cod4watcher/views/layouts" +import "gitea.henriburau.de/haw-lan/cod4watcher/views/components" + +type CaptureFormValues struct { + Host string + Port string + Name string +} + +templ CaptureForm(formValues CaptureFormValues, errors map[string]string) { + @layouts.Base() { +
+
+
Create new capture
+
+ + @components.Input(components.InputProps{ + Name: "name", + Value: formValues.Name, + Error: errors["name"], + Placeholder: "Name for the capture", + }) +
+
+ + @components.Input(components.InputProps{ + Name: "host", + Value: formValues.Host, + Error: errors["host"], + Placeholder: "Host-Address of the capture", + }) +
+
+ + @components.Input(components.InputProps{ + Name: "port", + Value: formValues.Port, + Error: errors["port"], + Placeholder: "Port for the capture", + }) +
+ +
+
+ } +} diff --git a/views/capture/capture_form_templ.go b/views/capture/capture_form_templ.go new file mode 100644 index 0000000..cab5598 --- /dev/null +++ b/views/capture/capture_form_templ.go @@ -0,0 +1,98 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.707 +package capture + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import "context" +import "io" +import "bytes" + +import "gitea.henriburau.de/haw-lan/cod4watcher/views/layouts" +import "gitea.henriburau.de/haw-lan/cod4watcher/views/components" + +type CaptureFormValues struct { + Host string + Port string + Name string +} + +func CaptureForm(formValues CaptureFormValues, errors map[string]string) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var2 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Create new capture
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = components.Input(components.InputProps{ + Name: "name", + Value: formValues.Name, + Error: errors["name"], + Placeholder: "Name for the capture", + }).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = components.Input(components.InputProps{ + Name: "host", + Value: formValues.Host, + Error: errors["host"], + Placeholder: "Host-Address of the capture", + }).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = components.Input(components.InputProps{ + Name: "port", + Value: formValues.Port, + Error: errors["port"], + Placeholder: "Port for the capture", + }).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer) + } + return templ_7745c5c3_Err + }) + templ_7745c5c3_Err = layouts.Base().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/views/capture/capture_templ.go b/views/capture/capture_templ.go index 9896812..812e5ec 100644 --- a/views/capture/capture_templ.go +++ b/views/capture/capture_templ.go @@ -33,7 +33,7 @@ func Capture(capture *models.Capture) templ.Component { templ_7745c5c3_Buffer = templ.GetBuffer() defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -46,7 +46,7 @@ func Capture(capture *models.Capture) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 2) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -59,7 +59,7 @@ func Capture(capture *models.Capture) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 3) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(":") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -72,7 +72,7 @@ func Capture(capture *models.Capture) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 4) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -80,7 +80,7 @@ func Capture(capture *models.Capture) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 5) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/views/capture/capture_templ.txt b/views/capture/capture_templ.txt deleted file mode 100644 index e526476..0000000 --- a/views/capture/capture_templ.txt +++ /dev/null @@ -1,5 +0,0 @@ -
-
-: -
-
diff --git a/views/components/capture_card.templ b/views/components/capture_card.templ index c6d96e0..2fada88 100644 --- a/views/components/capture_card.templ +++ b/views/components/capture_card.templ @@ -5,8 +5,9 @@ import "github.com/mergestat/timediff" import "fmt" templ CaptureCard(capture models.Capture) { - +
{ capture.Name }

{ timediff.TimeDiff(capture.Start) }

+
} diff --git a/views/components/capture_card_templ.go b/views/components/capture_card_templ.go index 25bf77c..d3ec73a 100644 --- a/views/components/capture_card_templ.go +++ b/views/components/capture_card_templ.go @@ -27,16 +27,16 @@ func CaptureCard(capture models.Capture) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -49,7 +49,7 @@ func CaptureCard(capture models.Capture) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 3) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -62,7 +62,7 @@ func CaptureCard(capture models.Capture) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 4) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/views/components/capture_card_templ.txt b/views/components/capture_card_templ.txt deleted file mode 100644 index fb71440..0000000 --- a/views/components/capture_card_templ.txt +++ /dev/null @@ -1,4 +0,0 @@ -
-

-

diff --git a/views/components/capture_table_templ.go b/views/components/capture_table_templ.go index 4a3f53e..01c525a 100644 --- a/views/components/capture_table_templ.go +++ b/views/components/capture_table_templ.go @@ -26,12 +26,12 @@ func CaptureTable(table models.ResultTable) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, header := range table.Header { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 2) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 5) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, row := range table.Rows { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 6) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, score := range row.Individual { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 9) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 11) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 12) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -44,7 +44,7 @@ func CaptureTable(table models.ResultTable) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 3) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -57,17 +57,17 @@ func CaptureTable(table models.ResultTable) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 4) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -80,7 +80,7 @@ func CaptureTable(table models.ResultTable) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 7) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -93,12 +93,12 @@ func CaptureTable(table models.ResultTable) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 8) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -111,17 +111,17 @@ func CaptureTable(table models.ResultTable) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 10) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/views/components/capture_table_templ.txt b/views/components/capture_table_templ.txt deleted file mode 100644 index ce1dfa0..0000000 --- a/views/components/capture_table_templ.txt +++ /dev/null @@ -1,12 +0,0 @@ -
- - - - - -
-
-
- - -
diff --git a/views/components/input.templ b/views/components/input.templ new file mode 100644 index 0000000..f032328 --- /dev/null +++ b/views/components/input.templ @@ -0,0 +1,15 @@ +package components + +type InputProps struct { + Name string + Value string + Error string + Placeholder string +} + +templ Input(props InputProps) { + + if props.Error != "" { + { props.Error } + } +} diff --git a/views/components/input_templ.go b/views/components/input_templ.go new file mode 100644 index 0000000..199e951 --- /dev/null +++ b/views/components/input_templ.go @@ -0,0 +1,113 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.707 +package components + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import "context" +import "io" +import "bytes" + +type InputProps struct { + Name string + Value string + Error string + Placeholder string +} + +func Input(props InputProps) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if props.Error != "" { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(props.Error) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/components/input.templ`, Line: 13, Col: 42} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/views/components/navbar_templ.go b/views/components/navbar_templ.go index b5b8350..24b0250 100644 --- a/views/components/navbar_templ.go +++ b/views/components/navbar_templ.go @@ -23,7 +23,7 @@ func Nagivation() templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/views/components/navbar_templ.txt b/views/components/navbar_templ.txt deleted file mode 100644 index cd25273..0000000 --- a/views/components/navbar_templ.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/views/layouts/base.templ b/views/layouts/base.templ index 8d33cab..80708e1 100644 --- a/views/layouts/base.templ +++ b/views/layouts/base.templ @@ -15,7 +15,7 @@ templ Base() { @components.Nagivation() -
+
{ children... }
diff --git a/views/layouts/base_templ.go b/views/layouts/base_templ.go index a405cff..0ba53c9 100644 --- a/views/layouts/base_templ.go +++ b/views/layouts/base_templ.go @@ -25,7 +25,7 @@ func Base() templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("CoD 4 Turnier-Tracker") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -33,7 +33,7 @@ func Base() templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 2) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -41,7 +41,7 @@ func Base() templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 3) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/views/layouts/base_templ.txt b/views/layouts/base_templ.txt deleted file mode 100644 index 97a872a..0000000 --- a/views/layouts/base_templ.txt +++ /dev/null @@ -1,3 +0,0 @@ -CoD 4 Turnier-Tracker -
-