├── .gitignore ├── LICENSE ├── Makefile ├── cmd ├── generate │ └── main.go └── server │ └── main.go ├── content ├── getting_started.md └── types.md ├── generated ├── component_code_map.json ├── component_example_code_map.json └── components.go ├── go.mod ├── go.sum ├── input.css ├── internal ├── components.go ├── echo.go ├── handler │ ├── components.go │ ├── error.go │ ├── htmx.go │ ├── input_validation.go │ ├── pages.go │ └── render.go ├── markdown.go ├── markdown │ └── markdown.go ├── model │ └── generation.go ├── settings.go └── views │ ├── components │ ├── accordion.templ │ ├── active_search.templ │ ├── alert.templ │ ├── anchor.templ │ ├── avatar.templ │ ├── banner.templ │ ├── breadcrumbs.templ │ ├── card.templ │ ├── carousel.templ │ ├── chat.templ │ ├── checkbox.templ │ ├── collapse.templ │ ├── combobox.templ │ ├── countdown.templ │ ├── date_picker.templ │ ├── diff.templ │ ├── drawer.templ │ ├── dropdown.templ │ ├── features.templ │ ├── file_input.templ │ ├── footer.templ │ ├── hero.templ │ ├── infinite_scroll.templ │ ├── input.templ │ ├── lazy_load.templ │ ├── menu.templ │ ├── modal.templ │ ├── pagination.templ │ ├── pricing.templ │ ├── radio.templ │ ├── range.templ │ ├── rating.templ │ ├── select.templ │ ├── skeleton.templ │ ├── stats.templ │ ├── status.templ │ ├── steps.templ │ ├── swap.templ │ ├── table.templ │ ├── tabs.templ │ ├── testimonial.templ │ ├── textarea.templ │ ├── time_slot_picker.templ │ ├── timeline.templ │ ├── toast.templ │ ├── toggle.templ │ └── tooltip.templ │ ├── custom │ └── toast.templ │ ├── examples │ ├── accordion.templ │ ├── active_search.templ │ ├── alert.templ │ ├── anchor.templ │ ├── avatar.templ │ ├── banner.templ │ ├── breadcrumbs.templ │ ├── card.templ │ ├── carousel.templ │ ├── chat.templ │ ├── checkbox.templ │ ├── collapse.templ │ ├── combobox.templ │ ├── countdown.templ │ ├── date_picker.templ │ ├── diff.templ │ ├── drawer.templ │ ├── dropdown.templ │ ├── features.templ │ ├── file_input.templ │ ├── footer.templ │ ├── hero.templ │ ├── infinite_scroll.templ │ ├── input.templ │ ├── lazy_load.templ │ ├── menu.templ │ ├── modal.templ │ ├── pagination.templ │ ├── pricing.templ │ ├── radio.templ │ ├── range.templ │ ├── rating.templ │ ├── select.templ │ ├── skeleton.templ │ ├── stats.templ │ ├── status.templ │ ├── steps.templ │ ├── swap.templ │ ├── table.templ │ ├── tabs.templ │ ├── testimonial.templ │ ├── textarea.templ │ ├── time_slot_picker.templ │ ├── timeline.templ │ ├── toast.templ │ ├── toggle.templ │ └── tooltip.templ │ ├── pages │ ├── base.templ │ ├── client_error.templ │ ├── component.templ │ ├── index.templ │ └── server_error.templ │ └── scripts │ └── copy_button.templ ├── package.json ├── public ├── favicon.ico └── static │ ├── css │ ├── chroma.css │ └── custom.css │ ├── images │ ├── avatar-reverse.jpg │ ├── avatar.jpg │ ├── diff1.png │ ├── diff2.png │ ├── goshipit-logo.png │ ├── goshipit.png │ ├── profile-long.jpg │ ├── profile.jpg │ └── templ.png │ └── js │ ├── alpine.js │ ├── datastar.js │ └── htmx.min.js ├── readme.md └── tailwind.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .env 3 | *_templ.go 4 | *_templ.txt 5 | bin/ 6 | node_modules/ 7 | package-lock.json 8 | public/static/css/tw.css 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tomi Haapalainen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := dev 2 | 3 | GOOS := "linux" 4 | GOARCH := "amd64" 5 | 6 | deploy: 7 | npx @tailwindcss/cli -i input.css -o ./public/static/css/tw.css --minify 8 | go run cmd/generate/main.go 9 | templ generate 10 | GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags "-s -w" -o bin/main cmd/server/main.go 11 | scp -r 'content' $(user)@$(ip):/opt/goshipit/ 12 | scp -r 'generated' $(user)@$(ip):/opt/goshipit/ 13 | scp -r 'public' $(user)@$(ip):/opt/goshipit/ 14 | ssh $(user)@$(ip) "sudo service goshipit stop" 15 | scp 'bin/main' $(user)@$(ip):/opt/goshipit/ 16 | ssh $(user)@$(ip) "sudo service goshipit start" 17 | 18 | gen: 19 | go run cmd/generate/main.go 20 | 21 | tw: 22 | @npx @tailwindcss/cli -i input.css -o ./public/static/css/tw.css --watch 23 | 24 | dev: gen 25 | @templ generate -watch -proxyport=7332 -proxy="http://localhost:8080" -open-browser=false -cmd="go run cmd/server/main.go" 26 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/haatos/goshipit/internal" 5 | "github.com/haatos/goshipit/internal/handler" 6 | "github.com/labstack/echo/v4" 7 | "github.com/labstack/echo/v4/middleware" 8 | 9 | _ "github.com/mattn/go-sqlite3" 10 | ) 11 | 12 | func main() { 13 | internal.ReadDotenv() 14 | internal.Settings = internal.NewSettings() 15 | 16 | e := echo.New() 17 | e.HTTPErrorHandler = handler.ErrorHandler 18 | loggerFormat := "${method} ${uri} [${status}] (${latency_human}) | ${short_file}:${line} | ${message}\n" 19 | e.Logger.SetHeader(loggerFormat) 20 | 21 | config := internal.GetRateLimiterConfig() 22 | e.Use(middleware.RateLimiterWithConfig(config)) 23 | 24 | e.Use( 25 | middleware.LoggerWithConfig(middleware.LoggerConfig{ 26 | Format: loggerFormat, 27 | }), 28 | middleware.GzipWithConfig(middleware.GzipConfig{ 29 | Skipper: middleware.DefaultSkipper, 30 | Level: 3, 31 | }), 32 | ) 33 | 34 | e.Static("/", "public") 35 | 36 | e.GET("/", handler.GetIndexPage) 37 | e.GET("/about", handler.GetAboutPage) 38 | e.GET("/get-started", handler.GetGettingStartedPage) 39 | e.GET("/types", handler.GetTypesPage) 40 | e.GET("/component-anchors", handler.GetComponentAnchors) 41 | e.GET("/privacy", handler.GetPrivacyPolicyPage) 42 | e.GET("/terms-of-service", handler.GetTermsOfServicePage) 43 | 44 | e.GET("/components/:category/:name", handler.GetComponentPage) 45 | e.GET("/components/search", handler.GetComponentSearch) 46 | 47 | // handlers for component examples 48 | e.POST("/validate/string/:name", handler.PostValidateString) 49 | e.GET("/infinite-scroll", handler.GetInfiniteScrollExample) 50 | e.GET("/infinite-scroll-rows", handler.GetInfiniteScrollExampleRows) 51 | e.GET("/active-search", handler.GetActiveSearchExample) 52 | e.GET("/lazy-load", handler.GetLazyLoadExample) 53 | e.GET("/models", handler.GetCascadingSelectExample) 54 | e.GET("/pagination-pages", handler.GetPaginationExamplePage) 55 | e.POST("/combobox/:name/:value", handler.PostCombobox) 56 | e.POST("/combobox-submit/:name", handler.PostComboboxSubmit) 57 | e.DELETE("/modal-confirm", handler.DeleteModalExample) 58 | e.POST("/datepicker/select", handler.PostDatePickerSelectDay) 59 | e.GET("/datepicker", handler.GetDatePicker) 60 | e.GET("/datepicker/monthpicker", handler.GetDatePickerMonthPicker) 61 | e.GET("/datepicker/yearpicker", handler.GetDatePickerYearPicker) 62 | e.GET("/timeslotpicker", handler.GetTimeSlotPicker) 63 | e.POST("/timeslotpicker/reserve", handler.PostTimeSlotPickerReserve) 64 | // handlers for component examples 65 | 66 | internal.GracefulShutdown(e, internal.Settings.Port) 67 | } 68 | -------------------------------------------------------------------------------- /content/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | To get started with goship.it in a new project using _Echo_ router: 4 | 5 | - Create a new folder for your project 6 | - Initialize the module by running 7 | - `go mod init github.com/my/package` 8 | - replace the package with your own repository! 9 | - Get Go modules: 10 | - `go get -u github.com/labstack/echo/v4` 11 | - `go get -u github.com/a-h/templ` 12 | - Install Templ CLI: 13 | - `go install github.com/a-h/templ/cmd/templ@latest` 14 | - Install TailwindCSS and DaisyUI: 15 | - `npm i -D tailwindcss@latest @tailwindcss/typography daisyui@latest` 16 | - Initialize TailwindCSS: 17 | - `npx tailwindcss init` 18 | - Configure `tailwind.config.js`, which the previous command generated, to look like this: 19 | 20 | ```javascript 21 | module.exports = { 22 | content: ["internal/views/**/*.templ"], 23 | theme: { 24 | extend: {}, 25 | }, 26 | }; 27 | ``` 28 | 29 | - Create `input.css` at the base of your project with the following contents: 30 | 31 | ```css 32 | @import "tailwindcss" source(none); 33 | @config "./tailwind.config.js"; 34 | @source "./internal/views/**/*.templ"; 35 | @plugin "daisyui"; 36 | ``` 37 | 38 | - Create `Makefile` at the base of your project with the following contents: 39 | 40 | ```make 41 | tw: 42 | @npx tailwindcss -i input.css -o public/static/css/tw.css --watch 43 | 44 | dev: 45 | @templ generate -watch -proxy="http://localhost:8080" -open-browser=false -cmd="go run main.go" 46 | ``` 47 | 48 | - Place the following rows in `main.go` (remember to update the components package import path to match your project): 49 | 50 | ```go 51 | package main 52 | 53 | import ( 54 | "embed" 55 | "log" 56 | "net/http" 57 | 58 | "/internal/views/components" 59 | 60 | "github.com/labstack/echo/v4" 61 | ) 62 | 63 | func main() { 64 | e := echo.New() 65 | 66 | e.Static("/", "public") 67 | 68 | e.GET("/", func(c echo.Context) error { 69 | accordion := components.AccordionExample() 70 | return render(accordion) 71 | }) 72 | 73 | e.Start(":8080") 74 | } 75 | 76 | func render(c echo.Context, component templ.Component) error { 77 | buf := templ.GetBuffer() 78 | defer templ.ReleaseBuffer(buf) 79 | 80 | if err := component.Render(c.Request().Context(), buf); err != nil { 81 | return err 82 | } 83 | return c.HTML(http.StatusOK, buf.String()) 84 | } 85 | 86 | ``` 87 | 88 | - Create the accordion component `internal/views/components/accordion.templ`: 89 | 90 | ```go 91 | package components 92 | 93 | type AccordionRowProps struct { 94 | Label string 95 | Type string 96 | Name string 97 | } 98 | 99 | templ AccordionRow(props AccordionRowProps) { 100 |
101 | 109 |
{ props.Label }
110 |
111 | { children... } 112 |
113 |
114 | } 115 | 116 | templ AccordionExample() { 117 | 118 | 119 | 120 | 121 | 122 | 123 | Document 124 | 125 | 126 |
127 |
128 | @AccordionRow(AccordionRowProps{Label: "Accordion row 1", Type: "checkbox"}) { 129 |

This is the first content

130 | } 131 | @AccordionRow(AccordionRowProps{Label: "Accordion row 2", Type: "checkbox"}) { 132 |

This is the second content

133 | } 134 |
135 |
136 | 137 | 138 | } 139 | ``` 140 | 141 | At this point, the filetree of your project should look something like this: 142 | 143 | ```sh 144 | . 145 | ├── Makefile 146 | ├── go.mod 147 | ├── go.sum 148 | ├── input.css 149 | ├── internal 150 | │ └── views 151 | ├── main.go 152 | ├── node_modules 153 | ├── package-lock.json 154 | ├── package.json 155 | ├── public 156 | │ └── static 157 | │ └── css 158 | │ └── tw.css 159 | └── tailwind.config.js 160 | ``` 161 | 162 | If you are using VSCode as your IDE, you should also add a `.vscode/settings.json` with the following contents (or place these settings in some other VSCode configuration file): 163 | 164 | ```json 165 | { 166 | "[templ]": { 167 | "editor.formatOnSave": true, 168 | "editor.defaultFormatter": "a-h.templ" 169 | }, 170 | "tailwindCSS.includeLanguages": { 171 | "templ": "html" 172 | }, 173 | "emmet.includeLanguages": { 174 | "templ": "html" 175 | } 176 | } 177 | ``` 178 | 179 | These will enable TailwindCSS autocompletions and HTML element autocompletions (emmet), as well as automatically formatting `.templ` files when saving. 180 | 181 | Finally, you can run the example application by running `make tw` and `make dev` in two separate terminals. The site with the accordion should now be visible at http://localhost:8080. 182 | -------------------------------------------------------------------------------- /content/types.md: -------------------------------------------------------------------------------- 1 | # Types 2 | 3 | ## Introduction 4 | 5 | Most components use a struct as the input argument. This provides a convenient way to pass default values to the components; struct fields initialize to the respective type's default value. We can use this feature to make some of the fields of a struct optional to make initialization less cumbersome. For example, we can pass the `components.AnchorProps` argument to an anchor element, ``: 6 | 7 | ```go 8 | package components 9 | 10 | type AnchorProps struct { 11 | Href string 12 | Label string 13 | LeftIcon templ.Component 14 | RightIcon templ.Component 15 | Attrs templ.Attributes 16 | Class string 17 | } 18 | 19 | templ Anchor(props AnchorProps) { 20 | 27 | if props.LeftIcon != nil { 28 |
29 | @props.LeftIcon 30 |
31 | } 32 | { props.Label } 33 | if props.RightIcon != nil { 34 |
35 | @props.RightIcon 36 |
37 | } 38 |
39 | } 40 | ``` 41 | 42 | Here we can define the anchor element to optionally have an `href` attribute, or if we choose, a `hx-get` instead: 43 | 44 | ```go 45 | @components.Anchor(components.AnchorProps{Href: "/"}) 46 | @components.Anchor(components.AnchorProps{Attrs: templ.Attributes{"hx-get": "/"}}) 47 | ``` 48 | 49 | ## Deprecated types 50 | 51 | ```go 52 | package model 53 | 54 | import ( 55 | "time" 56 | 57 | "github.com/a-h/templ" 58 | ) 59 | 60 | type Accordion struct { 61 | Label string 62 | Type string 63 | Name string 64 | } 65 | 66 | type ActiveSearchInput struct { 67 | ID string 68 | URL string 69 | Target string 70 | Input Input 71 | } 72 | 73 | type Anchor struct { 74 | Href string 75 | Label string 76 | LeftIcon templ.Component 77 | RightIcon templ.Component 78 | Attrs templ.Attributes 79 | Class string 80 | } 81 | 82 | type Avatar struct { 83 | AvatarClass string 84 | ContainerClass string 85 | Source string 86 | Placeholder string 87 | PlaceholderClass string 88 | } 89 | 90 | type Banner struct { 91 | Title templ.Component 92 | Description string 93 | CallToAction Button 94 | SecondaryCallToAction Button 95 | } 96 | 97 | type Button struct { 98 | Label string 99 | Attrs templ.Attributes 100 | } 101 | 102 | type Card struct { 103 | Title string 104 | Content string 105 | Source string 106 | Alt string 107 | Class string 108 | } 109 | 110 | type Chat struct { 111 | Messages []ChatMessage 112 | } 113 | 114 | type ChatMessage struct { 115 | AvatarURL string 116 | Sender string 117 | Time string 118 | Message string 119 | Footer string 120 | Location string 121 | Class string 122 | } 123 | 124 | type Checkbox struct { 125 | ID string 126 | Before string 127 | After string 128 | Name string 129 | Checked bool 130 | Class string 131 | Attrs templ.Attributes 132 | } 133 | 134 | type Collapse struct { 135 | Class string 136 | Title string 137 | TitleClass string 138 | ContentClass string 139 | } 140 | 141 | type Combobox struct { 142 | Label string 143 | Name string 144 | URL string 145 | Options []string 146 | Selected []string 147 | } 148 | 149 | type CompanyInfo struct { 150 | Icon templ.Component 151 | Name string 152 | Description string 153 | Copyright string 154 | } 155 | 156 | type DatePicker struct { 157 | Year int 158 | Month int 159 | Selected time.Time 160 | StartOfWeek time.Weekday 161 | } 162 | 163 | func (dp DatePicker) Days() []time.Time { 164 | days := make([]time.Time, 0, 31) 165 | now := time.Now().UTC() 166 | start := time.Date(dp.Year, time.Month(dp.Month), 1, 0, 0, 0, 0, now.Location()) 167 | end := start.AddDate(0, 1, -1) 168 | for end.Weekday() != dp.StartOfWeek { 169 | end = end.AddDate(0, 0, 1) 170 | } 171 | end = end.AddDate(0, 0, -1) 172 | 173 | for start.Weekday() != dp.StartOfWeek { 174 | start = start.AddDate(0, 0, -1) 175 | } 176 | for !start.After(end) { 177 | days = append(days, start) 178 | start = start.AddDate(0, 0, 1) 179 | } 180 | return days 181 | } 182 | 183 | func (dp DatePicker) Months() []time.Time { 184 | months := make([]time.Time, 12) 185 | for i := 1; i <= 12; i++ { 186 | dt := time.Date(dp.Year, time.Month(i), 1, 0, 0, 0, 0, time.Now().Location()) 187 | months[i-1] = dt 188 | } 189 | return months 190 | } 191 | 192 | type Dropdown struct { 193 | Label string 194 | Class string 195 | ListClass string 196 | Items []DropdownItem 197 | } 198 | 199 | type DropdownItem struct { 200 | Label string 201 | Attrs templ.Attributes 202 | } 203 | 204 | type Feature struct { 205 | Icon templ.Component 206 | Title string 207 | Description string 208 | URL string 209 | } 210 | 211 | type Image struct { 212 | Source string 213 | Alt string 214 | } 215 | 216 | type Input struct { 217 | ID string 218 | Type string // defaults to "text" 219 | Label string 220 | Name string 221 | Value string 222 | Placeholder string 223 | Err string 224 | Attrs templ.Attributes 225 | Class string 226 | Icon templ.Component 227 | Disabled bool 228 | DisabledMessage string 229 | Required bool 230 | } 231 | 232 | type PaginationItem struct { 233 | URL string 234 | Page int 235 | Low int 236 | High int 237 | MaxPages int 238 | } 239 | 240 | type Price struct { 241 | Title string 242 | Description string 243 | Price string 244 | Per string 245 | IncludedFeatures []string 246 | ExcludedFeatures []string 247 | CallToAction Button 248 | Footer templ.Component 249 | } 250 | 251 | type Radio struct { 252 | Name string 253 | Values map[string]string 254 | Class string 255 | } 256 | 257 | type Range struct { 258 | ID string 259 | Label string 260 | Name string 261 | Value int 262 | Min int 263 | Max int 264 | Step int 265 | Class string 266 | } 267 | 268 | type Rating struct { 269 | Name string 270 | Min int 271 | Max int 272 | Class string 273 | Value int 274 | } 275 | 276 | type Script struct { 277 | Source string 278 | Defer bool 279 | } 280 | 281 | type Select struct { 282 | ID string 283 | Label string 284 | Name string 285 | Options []SelectOption 286 | Attrs templ.Attributes 287 | Class string 288 | } 289 | 290 | type SelectOption struct { 291 | Label string 292 | Value string 293 | Selected bool 294 | Disabled bool 295 | } 296 | 297 | type Stat struct { 298 | Title string 299 | Value string 300 | Description string 301 | } 302 | 303 | type Status struct { 304 | Code int 305 | Title string 306 | Description string 307 | ReturnButton Button 308 | } 309 | 310 | type Swap struct { 311 | On templ.Component 312 | Off templ.Component 313 | Class string 314 | } 315 | 316 | type Tabs struct { 317 | Name string 318 | Class string 319 | Tabs []Tab 320 | ContentClass string 321 | } 322 | 323 | type Tab struct { 324 | Label string 325 | Content templ.Component 326 | } 327 | 328 | type Testimonial struct { 329 | Avatar templ.Component 330 | Name string 331 | Rating int 332 | Content string 333 | } 334 | 335 | type Textarea struct { 336 | ID string 337 | Label string 338 | Name string 339 | Placeholder string 340 | Value string 341 | Rows int 342 | Err string 343 | Class string 344 | Attrs templ.Attributes 345 | } 346 | 347 | type TimelineItem struct { 348 | Start string 349 | Middle templ.Component 350 | End string 351 | } 352 | 353 | type Toast struct { 354 | Name string 355 | ToastClass string 356 | AlertClass string 357 | } 358 | 359 | type Toggle struct { 360 | ID string 361 | Before string 362 | After string 363 | Name string 364 | Checked bool 365 | Class string 366 | Highlight bool 367 | Attrs templ.Attributes 368 | } 369 | 370 | type Tooltip struct { 371 | Tip string 372 | Class string 373 | } 374 | ``` 375 | -------------------------------------------------------------------------------- /generated/components.go: -------------------------------------------------------------------------------- 1 | package generated 2 | 3 | import ( 4 | "github.com/a-h/templ" 5 | "github.com/haatos/goshipit/internal/views/examples" 6 | ) 7 | 8 | var ExampleComponents = map[string]templ.Component{ 9 | "AccordionWithCheckbox": examples.AccordionWithCheckbox(), 10 | "AccordionWithRadio": examples.AccordionWithRadio(), 11 | "ActiveSearchExampleTable": examples.ActiveSearchExampleTable(), 12 | "AlertInfoExample": examples.AlertInfoExample(), 13 | "AlertSuccessExample": examples.AlertSuccessExample(), 14 | "AlertWarningExample": examples.AlertWarningExample(), 15 | "AlertErrorExample": examples.AlertErrorExample(), 16 | "BasicAnchor": examples.BasicAnchor(), 17 | "PrimaryAnchor": examples.PrimaryAnchor(), 18 | "AnchorWithIcon": examples.AnchorWithIcon(), 19 | "SocialAnchors": examples.SocialAnchors(), 20 | "MultipleAvatarSizes": examples.MultipleAvatarSizes(), 21 | "GroupOfAvatars": examples.GroupOfAvatars(), 22 | "OnlineAndOffline": examples.OnlineAndOffline(), 23 | "BannerExample": examples.BannerExample(), 24 | "BreadcrumbsExample": examples.BreadcrumbsExample(), 25 | "BasicCard": examples.BasicCard(), 26 | "BasicCardWithImage": examples.BasicCardWithImage(), 27 | "CarouselExample": examples.CarouselExample(), 28 | "BasicChat": examples.BasicChat(), 29 | "DifferentSizeCheckboxes": examples.DifferentSizeCheckboxes(), 30 | "PrimaryCheckbox": examples.PrimaryCheckbox(), 31 | "CollapseExample": examples.CollapseExample(), 32 | "BasicCombobox": examples.BasicCombobox(), 33 | "CountdownExample": examples.CountdownExample(), 34 | "BasicDatePicker": examples.BasicDatePicker(), 35 | "ImageDiff": examples.ImageDiff(), 36 | "BasicDrawer": examples.BasicDrawer(), 37 | "BasicDropdown": examples.BasicDropdown(), 38 | "FeaturesExample": examples.FeaturesExample(), 39 | "DifferentSizeFileInputs": examples.DifferentSizeFileInputs(), 40 | "BasicFooterWithLinks": examples.BasicFooterWithLinks(), 41 | "BasicHero": examples.BasicHero(), 42 | "InfiniteScrollTableExample": examples.InfiniteScrollTableExample(), 43 | "DifferentSizeInputs": examples.DifferentSizeInputs(), 44 | "IntegerInput": examples.IntegerInput(), 45 | "DecimalInput": examples.DecimalInput(), 46 | "PasswordFieldWithValidator": examples.PasswordFieldWithValidator(), 47 | "EmailFieldWithValidator": examples.EmailFieldWithValidator(), 48 | "SignUpFormExample": examples.SignUpFormExample(), 49 | "LazyLoadExample": examples.LazyLoadExample(), 50 | "MenuExample": examples.MenuExample(), 51 | "MenuWithSubmenusExample": examples.MenuWithSubmenusExample(), 52 | "DashboardMenuExample": examples.DashboardMenuExample(), 53 | "BasicModal": examples.BasicModal(), 54 | "MultipleModals": examples.MultipleModals(), 55 | "ModalWithAction": examples.ModalWithAction(), 56 | "ModalConfirmDelete": examples.ModalConfirmDelete(), 57 | "BasicPaginationExample": examples.BasicPaginationExample(), 58 | "PricingExample": examples.PricingExample(), 59 | "PricingWithPromotionExample": examples.PricingWithPromotionExample(), 60 | "DefaultRadio": examples.DefaultRadio(), 61 | "PrimaryRadio": examples.PrimaryRadio(), 62 | "BasicRange": examples.BasicRange(), 63 | "DatastarRange": examples.DatastarRange(), 64 | "RatingFromOneToFive": examples.RatingFromOneToFive(), 65 | "RatingFromZeroToFive": examples.RatingFromZeroToFive(), 66 | "DifferentSizeSelects": examples.DifferentSizeSelects(), 67 | "CascadingSelect": examples.CascadingSelect(), 68 | "SkeletonExample": examples.SkeletonExample(), 69 | "BasicStats": examples.BasicStats(), 70 | "StatusNotFound": examples.StatusNotFound(), 71 | "StatusForbidden": examples.StatusForbidden(), 72 | "StatusUnauthorized": examples.StatusUnauthorized(), 73 | "StatusInternalServerError": examples.StatusInternalServerError(), 74 | "BasicSteps": examples.BasicSteps(), 75 | "BasicSwap": examples.BasicSwap(), 76 | "BasicTable": examples.BasicTable(), 77 | "BasicTabs": examples.BasicTabs(), 78 | "TestimonialGridExample": examples.TestimonialGridExample(), 79 | "BasicTextarea": examples.BasicTextarea(), 80 | "DifferentSizeTextareas": examples.DifferentSizeTextareas(), 81 | "BasicTextareaWithError": examples.BasicTextareaWithError(), 82 | "BasicTimeSlotPicker": examples.BasicTimeSlotPicker(), 83 | "BasicTimeline": examples.BasicTimeline(), 84 | "InfoToast": examples.InfoToast(), 85 | "WarningToast": examples.WarningToast(), 86 | "ErrorToast": examples.ErrorToast(), 87 | "InfoToastConfirm": examples.InfoToastConfirm(), 88 | "DifferentSizeToggles": examples.DifferentSizeToggles(), 89 | "PrimaryToggle": examples.PrimaryToggle(), 90 | "PrimaryToggleWithHighlight": examples.PrimaryToggleWithHighlight(), 91 | "BasicTooltip": examples.BasicTooltip(), 92 | "BasicTooltipError": examples.BasicTooltipError(), 93 | } 94 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/haatos/goshipit 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/PuerkitoBio/goquery v1.10.1 7 | github.com/a-h/templ v0.3.865 8 | github.com/alecthomas/chroma v0.10.0 9 | github.com/labstack/echo/v4 v4.12.0 10 | github.com/labstack/gommon v0.4.2 11 | github.com/mattn/go-sqlite3 v1.14.24 12 | github.com/russross/blackfriday v1.6.0 13 | golang.org/x/time v0.7.0 14 | ) 15 | 16 | require ( 17 | github.com/andybalholm/cascadia v1.3.3 // indirect 18 | github.com/dlclark/regexp2 v1.11.4 // indirect 19 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 20 | github.com/mattn/go-colorable v0.1.13 // indirect 21 | github.com/mattn/go-isatty v0.0.20 // indirect 22 | github.com/stretchr/testify v1.9.0 // indirect 23 | github.com/valyala/bytebufferpool v1.0.0 // indirect 24 | github.com/valyala/fasttemplate v1.2.2 // indirect 25 | golang.org/x/crypto v0.37.0 // indirect 26 | golang.org/x/net v0.39.0 // indirect 27 | golang.org/x/sys v0.32.0 // indirect 28 | golang.org/x/text v0.24.0 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /input.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss" source(none); 2 | @plugin "@tailwindcss/typography"; 3 | @config "./tailwind.config.js"; 4 | @source "./internal/views/**/*.templ"; 5 | @plugin "daisyui" { 6 | themes: 7 | light --default, 8 | dark --prefersdark; 9 | } 10 | @plugin "daisyui/theme" { 11 | name: "light"; 12 | default: true; 13 | prefersdark: false; 14 | color-scheme: "light"; 15 | --color-base-100: oklch(92% 0 0); 16 | --color-base-200: oklch(87% 0 0); 17 | --color-base-300: oklch(70% 0 0); 18 | --color-base-content: oklch(0% 0 0); 19 | --color-primary: oklch(55% 0.135 66.442); 20 | --color-primary-content: oklch(98% 0.018 155.826); 21 | --color-secondary: oklch(53% 0.157 131.589); 22 | --color-secondary-content: oklch(98% 0.031 120.757); 23 | --color-accent: oklch(64% 0.222 41.116); 24 | --color-accent-content: oklch(98% 0.016 73.684); 25 | --color-neutral: oklch(43% 0 0); 26 | --color-neutral-content: oklch(98% 0 0); 27 | --color-info: oklch(52% 0.105 223.128); 28 | --color-info-content: oklch(98% 0.019 200.873); 29 | --color-success: oklch(50% 0.118 165.612); 30 | --color-success-content: oklch(98% 0.018 155.826); 31 | --color-warning: oklch(55% 0.195 38.402); 32 | --color-warning-content: oklch(98% 0.016 73.684); 33 | --color-error: oklch(52% 0.223 3.958); 34 | --color-error-content: oklch(97% 0.014 343.198); 35 | --radius-selector: 0.5rem; 36 | --radius-field: 0.5rem; 37 | --radius-box: 0.5rem; 38 | --size-selector: 0.28125rem; 39 | --size-field: 0.28125rem; 40 | --border: 1px; 41 | --depth: 1; 42 | --noise: 0; 43 | } 44 | @plugin "daisyui/theme" { 45 | name: "dark"; 46 | default: false; 47 | prefersdark: true; 48 | color-scheme: "dark"; 49 | --color-base-100: oklch(26% 0 0); 50 | --color-base-200: oklch(20% 0 0); 51 | --color-base-300: oklch(14% 0 0); 52 | --color-base-content: oklch(97% 0 0); 53 | --color-primary: oklch(79% 0.184 86.047); 54 | --color-primary-content: oklch(28% 0.066 53.813); 55 | --color-secondary: oklch(64% 0.2 131.684); 56 | --color-secondary-content: oklch(98% 0.031 120.757); 57 | --color-accent: oklch(64% 0.222 41.116); 58 | --color-accent-content: oklch(98% 0.016 73.684); 59 | --color-neutral: oklch(14% 0 0); 60 | --color-neutral-content: oklch(98% 0 0); 61 | --color-info: oklch(71% 0.143 215.221); 62 | --color-info-content: oklch(98% 0.019 200.873); 63 | --color-success: oklch(72% 0.219 149.579); 64 | --color-success-content: oklch(98% 0.018 155.826); 65 | --color-warning: oklch(70% 0.213 47.604); 66 | --color-warning-content: oklch(98% 0.016 73.684); 67 | --color-error: oklch(65% 0.241 354.308); 68 | --color-error-content: oklch(97% 0.014 343.198); 69 | --radius-selector: 0.5rem; 70 | --radius-field: 0.5rem; 71 | --radius-box: 0.5rem; 72 | --size-selector: 0.28125rem; 73 | --size-field: 0.28125rem; 74 | --border: 1px; 75 | --depth: 1; 76 | --noise: 0; 77 | } 78 | 79 | -------------------------------------------------------------------------------- /internal/components.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | 8 | "github.com/haatos/goshipit/internal/model" 9 | ) 10 | 11 | var ComponentCodeMap model.ComponentCodeMap 12 | var ComponentExampleCodeMap model.ComponentExampleCodeMap 13 | 14 | func init() { 15 | getComponentCodeMap() 16 | getComponentExampleCodeMap() 17 | } 18 | 19 | func getComponentCodeMap() { 20 | b, err := os.ReadFile("generated/component_code_map.json") 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | 25 | if err := json.Unmarshal(b, &ComponentCodeMap); err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | for k := range ComponentCodeMap { 30 | for i := range ComponentCodeMap[k] { 31 | ComponentCodeMap[k][i].Label = SnakeCaseToCapitalized(ComponentCodeMap[k][i].Name) 32 | } 33 | } 34 | } 35 | 36 | func getComponentExampleCodeMap() { 37 | b, err := os.ReadFile("generated/component_example_code_map.json") 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | if err := json.Unmarshal(b, &ComponentExampleCodeMap); err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | for k := range ComponentExampleCodeMap { 47 | for i := range ComponentExampleCodeMap[k] { 48 | ComponentExampleCodeMap[k][i].Label = SnakeCaseToCapitalized(ComponentExampleCodeMap[k][i].Name) 49 | } 50 | } 51 | } 52 | 53 | func SnakeCaseToCapitalized(s string) string { 54 | b := []byte(s) 55 | for i := range b { 56 | if i == 0 || (i > 0 && b[i-1] == ' ') { 57 | b[i] = b[i] - ('a' - 'A') 58 | } 59 | if b[i] == '_' { 60 | b[i] = ' ' 61 | } 62 | } 63 | return string(b) 64 | } 65 | -------------------------------------------------------------------------------- /internal/echo.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "os" 7 | "os/signal" 8 | "time" 9 | 10 | "github.com/labstack/echo/v4" 11 | "github.com/labstack/echo/v4/middleware" 12 | "golang.org/x/time/rate" 13 | ) 14 | 15 | func GracefulShutdown(e *echo.Echo, port string) { 16 | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) 17 | defer stop() 18 | 19 | go func() { 20 | if err := e.Start(port); err != nil && err != http.ErrServerClosed { 21 | e.Logger.Fatal("shutting down the server") 22 | } 23 | }() 24 | 25 | <-ctx.Done() 26 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 27 | defer cancel() 28 | if err := e.Shutdown(ctx); err != nil { 29 | e.Logger.Fatal(err) 30 | } 31 | } 32 | 33 | func GetRateLimiterConfig() middleware.RateLimiterConfig { 34 | return middleware.RateLimiterConfig{ 35 | Skipper: middleware.DefaultSkipper, 36 | Store: middleware.NewRateLimiterMemoryStoreWithConfig( 37 | middleware.RateLimiterMemoryStoreConfig{Rate: rate.Limit(10), Burst: 30, ExpiresIn: 3 * time.Minute}, 38 | ), 39 | IdentifierExtractor: func(ctx echo.Context) (string, error) { 40 | id := ctx.RealIP() 41 | return id, nil 42 | }, 43 | ErrorHandler: func(context echo.Context, err error) error { 44 | return context.JSON(http.StatusForbidden, nil) 45 | }, 46 | DenyHandler: func(context echo.Context, identifier string, err error) error { 47 | return context.JSON(http.StatusTooManyRequests, nil) 48 | }, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/handler/error.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/haatos/goshipit/internal/views/pages" 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func ErrorHandler(err error, c echo.Context) { 12 | c.Logger().Errorf("Handler error: %+v\n", err) 13 | switch e := err.(type) { 14 | case ErrorToast: 15 | renderErrorConfirm(c, e.Status, e.Messages) 16 | case *echo.HTTPError: 17 | switch e.Code { 18 | case http.StatusNotFound: 19 | render(c, e.Code, pages.NotFound()) 20 | case http.StatusInternalServerError: 21 | render(c, e.Code, pages.InternalServerError()) 22 | case http.StatusForbidden: 23 | render(c, e.Code, pages.Forbidden("Invalid permissions to view this page.")) 24 | } 25 | } 26 | } 27 | 28 | type ErrorToast struct { 29 | Status int 30 | Messages []string 31 | } 32 | 33 | func (te ErrorToast) Error() string { 34 | return strings.Join(te.Messages, ", ") 35 | } 36 | 37 | func newErrorToast(status int, messages ...string) ErrorToast { 38 | return ErrorToast{ 39 | Status: status, 40 | Messages: messages, 41 | } 42 | } 43 | 44 | func NotFound(c echo.Context) error { 45 | return render(c, http.StatusNotFound, pages.NotFound()) 46 | } 47 | -------------------------------------------------------------------------------- /internal/handler/htmx.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import "github.com/labstack/echo/v4" 4 | 5 | func hxRetarget(c echo.Context, target string) { 6 | c.Response().Writer.Header().Set("HX-Retarget", target) 7 | } 8 | 9 | func hxReswap(c echo.Context, swap string) { 10 | c.Response().Writer.Header().Set("HX-Reswap", swap) 11 | } 12 | 13 | func isHXRequest(c echo.Context) bool { 14 | return c.Request().Header.Get("hx-request") != "" 15 | } 16 | -------------------------------------------------------------------------------- /internal/handler/input_validation.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "regexp" 7 | "strings" 8 | "unicode" 9 | 10 | "github.com/haatos/goshipit/internal/views/components" 11 | "github.com/labstack/echo/v4" 12 | ) 13 | 14 | var emailRegexp = regexp.MustCompile(`^[^@]+@[^@]+\.[^@]+$`) 15 | 16 | var StringValidations = map[string]func(data string) string{ 17 | "notempty": func(data string) string { 18 | data = strings.TrimSpace(data) 19 | if data == "" { 20 | return "must not be empty" 21 | } 22 | return "" 23 | }, 24 | "email": func(data string) string { 25 | data = strings.TrimSpace(data) 26 | if !emailRegexp.Match([]byte(data)) { 27 | return "must be valid" 28 | } 29 | return "" 30 | }, 31 | "hasupper": func(data string) string { 32 | for _, r := range data { 33 | if unicode.IsUpper(r) { 34 | return "" 35 | } 36 | } 37 | return "must contain a uppercase letter" 38 | }, 39 | "haslower": func(data string) string { 40 | for _, r := range data { 41 | if unicode.IsLower(r) { 42 | return "" 43 | } 44 | } 45 | return "must contain a lowercase letter" 46 | }, 47 | "hasdigit": func(data string) string { 48 | for _, r := range data { 49 | if unicode.IsNumber(r) { 50 | return "" 51 | } 52 | } 53 | return "must contain a number" 54 | }, 55 | "hasspecial": func(data string) string { 56 | chars := `§!"@#£¤$%&/{([=?+\'*<>,;.:-_])}` 57 | if strings.ContainsAny(data, chars) { 58 | return "" 59 | } 60 | return fmt.Sprintf("must contain one of %s", chars) 61 | }, 62 | } 63 | 64 | func PostValidateString(c echo.Context) error { 65 | name := c.Param("name") 66 | value := c.FormValue(name) 67 | validations := c.QueryParams()["v"] 68 | 69 | errors := make([]string, 0, len(validations)) 70 | for _, validation := range validations { 71 | if res := StringValidations[validation](value); res != "" { 72 | errors = append(errors, res) 73 | } 74 | } 75 | 76 | e := strings.Join(errors, ", ") 77 | 78 | if e != "" { 79 | return render(c, http.StatusUnprocessableEntity, components.PlainText(e)) 80 | } 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /internal/handler/pages.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/haatos/goshipit/internal" 9 | "github.com/haatos/goshipit/internal/markdown" 10 | "github.com/haatos/goshipit/internal/views/pages" 11 | "github.com/labstack/echo/v4" 12 | ) 13 | 14 | var gettingStartedHTML string 15 | var typesHTML string 16 | 17 | const ( 18 | contentTypesMarkdownPath = "content/types.md" 19 | gettingStartedMarkdownPath = "content/getting_started.md" 20 | ) 21 | 22 | func getGettingStartedHTML() { 23 | if gettingStartedHTML != "" { 24 | return 25 | } 26 | 27 | pageContent, err := os.ReadFile(gettingStartedMarkdownPath) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | gettingStartedHTML = markdown.GetHTMLFromMarkdown(pageContent) 33 | } 34 | 35 | func getTypesHTML() { 36 | if typesHTML != "" { 37 | return 38 | } 39 | 40 | pageContent, err := os.ReadFile(contentTypesMarkdownPath) 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | 45 | typesHTML = markdown.GetHTMLFromMarkdown(pageContent) 46 | } 47 | 48 | func GetIndexPage(c echo.Context) error { 49 | if isHXRequest(c) { 50 | return render(c, http.StatusOK, pages.IndexPageContent()) 51 | } 52 | return render(c, http.StatusOK, pages.IndexPage()) 53 | } 54 | 55 | func GetAboutPage(c echo.Context) error { 56 | if isHXRequest(c) { 57 | return render(c, http.StatusOK, pages.AboutPageMain()) 58 | } 59 | return render(c, http.StatusOK, pages.AboutPage()) 60 | } 61 | 62 | func GetGettingStartedPage(c echo.Context) error { 63 | getGettingStartedHTML() 64 | 65 | if isHXRequest(c) { 66 | return render(c, http.StatusOK, pages.GettingStartedPageMain(gettingStartedHTML)) 67 | } 68 | return render(c, http.StatusOK, pages.GettingStartedPage(gettingStartedHTML)) 69 | } 70 | 71 | func GetTypesPage(c echo.Context) error { 72 | getTypesHTML() 73 | 74 | if isHXRequest(c) { 75 | return render(c, http.StatusOK, pages.TypesPageMain(typesHTML)) 76 | } 77 | return render(c, http.StatusOK, pages.TypesPage(typesHTML)) 78 | } 79 | 80 | func GetPrivacyPolicyPage(c echo.Context) error { 81 | if isHXRequest(c) { 82 | return render( 83 | c, http.StatusOK, 84 | pages.PrivacyMain(internal.Settings.Domain, internal.Settings.ContactEmail)) 85 | } 86 | return render( 87 | c, http.StatusOK, 88 | pages.PrivacyPage(internal.Settings.Domain, internal.Settings.ContactEmail)) 89 | } 90 | 91 | func GetTermsOfServicePage(c echo.Context) error { 92 | if isHXRequest(c) { 93 | return render( 94 | c, http.StatusOK, 95 | pages.TermsOfServiceMain(internal.Settings.Domain, internal.Settings.ContactEmail)) 96 | } 97 | return render( 98 | c, http.StatusOK, 99 | pages.TermsOfService(internal.Settings.Domain, internal.Settings.ContactEmail)) 100 | } 101 | -------------------------------------------------------------------------------- /internal/handler/render.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/a-h/templ" 7 | "github.com/haatos/goshipit/internal/views/custom" 8 | "github.com/labstack/echo/v4" 9 | "github.com/labstack/gommon/log" 10 | ) 11 | 12 | func render(c echo.Context, status int, com templ.Component) error { 13 | buf := templ.GetBuffer() 14 | defer templ.ReleaseBuffer(buf) 15 | 16 | if err := com.Render(c.Request().Context(), buf); err != nil { 17 | return err 18 | } 19 | 20 | return c.HTML(status, buf.String()) 21 | } 22 | 23 | func renderErrorConfirm(c echo.Context, status int, errs []string) error { 24 | hxRetarget(c, "body") 25 | hxReswap(c, "beforeend") 26 | return render(c, status, custom.ToastErrorConfirm(errs...)) 27 | } 28 | 29 | func renderInfoFade(c echo.Context, status int, messages []string) error { 30 | hxRetarget(c, "body") 31 | hxReswap(c, "beforeend") 32 | return render(c, status, custom.HXToastInfoFade(messages...)) 33 | } 34 | 35 | func getHTMLFromComponent(com templ.Component) string { 36 | buf := templ.GetBuffer() 37 | defer templ.ReleaseBuffer(buf) 38 | 39 | if err := com.Render(context.Background(), buf); err != nil { 40 | log.Error(err) 41 | } 42 | 43 | return buf.String() 44 | } 45 | -------------------------------------------------------------------------------- /internal/markdown.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "log" 7 | "strings" 8 | 9 | "github.com/PuerkitoBio/goquery" 10 | chromaHTML "github.com/alecthomas/chroma/formatters/html" 11 | "github.com/alecthomas/chroma/lexers" 12 | "github.com/alecthomas/chroma/styles" 13 | "github.com/russross/blackfriday" 14 | ) 15 | 16 | func GetHTMLFromMarkdown(markdown []byte) string { 17 | htmlBytes := blackfriday.MarkdownCommon(markdown) 18 | replaced, err := replaceCodeParts(htmlBytes) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | 23 | return replaced 24 | } 25 | 26 | func replaceCodeParts(mdFile []byte) (string, error) { 27 | byteReader := bytes.NewReader(mdFile) 28 | doc, err := goquery.NewDocumentFromReader(byteReader) 29 | if err != nil { 30 | return "", err 31 | } 32 | var hlErr error 33 | doc.Find("code[class*=\"language-\"]").Each(func(i int, s *goquery.Selection) { 34 | if hlErr != nil { 35 | return 36 | } 37 | class, _ := s.Attr("class") 38 | lang := strings.TrimPrefix(class, "language-") 39 | oldCode := s.Text() 40 | lexer := lexers.Get(lang) 41 | formatter := chromaHTML.New(chromaHTML.WithClasses(true)) 42 | iterator, err := lexer.Tokenise(nil, string(oldCode)) 43 | if err != nil { 44 | hlErr = err 45 | return 46 | } 47 | b := bytes.Buffer{} 48 | buf := bufio.NewWriter(&b) 49 | if err := formatter.Format(buf, styles.GitHub, iterator); err != nil { 50 | hlErr = err 51 | return 52 | } 53 | if err := buf.Flush(); err != nil { 54 | hlErr = err 55 | return 56 | } 57 | s.SetHtml(b.String()) 58 | }) 59 | if hlErr != nil { 60 | return "", hlErr 61 | } 62 | new, err := doc.Html() 63 | if err != nil { 64 | return "", err 65 | } 66 | return new, nil 67 | } 68 | -------------------------------------------------------------------------------- /internal/markdown/markdown.go: -------------------------------------------------------------------------------- 1 | package markdown 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "log" 7 | "strings" 8 | 9 | "github.com/PuerkitoBio/goquery" 10 | chromaHTML "github.com/alecthomas/chroma/formatters/html" 11 | "github.com/alecthomas/chroma/lexers" 12 | "github.com/alecthomas/chroma/styles" 13 | "github.com/russross/blackfriday" 14 | ) 15 | 16 | func GetHTMLFromMarkdown(markdown []byte) string { 17 | htmlBytes := blackfriday.MarkdownCommon(markdown) 18 | replaced, err := replaceCodeParts(htmlBytes) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | 23 | return replaced 24 | } 25 | 26 | func replaceCodeParts(mdFile []byte) (string, error) { 27 | byteReader := bytes.NewReader(mdFile) 28 | doc, err := goquery.NewDocumentFromReader(byteReader) 29 | if err != nil { 30 | return "", err 31 | } 32 | var hlErr error 33 | doc.Find("code[class*=\"language-\"]").Each(func(i int, s *goquery.Selection) { 34 | if hlErr != nil { 35 | return 36 | } 37 | class, _ := s.Attr("class") 38 | lang := strings.TrimPrefix(class, "language-") 39 | oldCode := s.Text() 40 | lexer := lexers.Get(lang) 41 | formatter := chromaHTML.New(chromaHTML.WithClasses(true)) 42 | iterator, err := lexer.Tokenise(nil, string(oldCode)) 43 | if err != nil { 44 | hlErr = err 45 | return 46 | } 47 | b := bytes.Buffer{} 48 | buf := bufio.NewWriter(&b) 49 | if err := formatter.Format(buf, styles.GitHub, iterator); err != nil { 50 | hlErr = err 51 | return 52 | } 53 | if err := buf.Flush(); err != nil { 54 | hlErr = err 55 | return 56 | } 57 | s.SetHtml(b.String()) 58 | }) 59 | if hlErr != nil { 60 | return "", hlErr 61 | } 62 | new, err := doc.Html() 63 | if err != nil { 64 | return "", err 65 | } 66 | return new, nil 67 | } 68 | 69 | func CodeSliceToMarkdown(s []string) string { 70 | if s == nil { 71 | return "" 72 | } 73 | n := make([]string, len(s)+2) 74 | n[0] = "```go" 75 | n[len(s)+2-1] = "```" 76 | copy(n[1:len(s)+1], s) 77 | return strings.Join(n, "\n") 78 | } 79 | -------------------------------------------------------------------------------- /internal/model/generation.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type ComponentCode struct { 4 | Name string `json:"name"` 5 | Code string `json:"code,omitempty"` 6 | Handler string `json:"handler,omitempty"` 7 | Title string `json:"title,omitempty"` 8 | Description string `json:"description,omitempty"` 9 | DaisyUIURL string `json:"daisy_ui_url,omitempty"` 10 | 11 | Label string `json:"-"` 12 | } 13 | 14 | type ComponentCodeMap map[string][]ComponentCode 15 | 16 | type ComponentExampleCodeMap map[string][]ComponentCode 17 | -------------------------------------------------------------------------------- /internal/settings.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bufio" 5 | "log" 6 | "log/slog" 7 | "os" 8 | "regexp" 9 | "strings" 10 | ) 11 | 12 | var Settings *AppSettings 13 | 14 | func NewSettings() *AppSettings { 15 | settings := AppSettings{ 16 | Title: "goship.it", 17 | ContactEmail: os.Getenv("CONTACT_EMAIL"), 18 | Domain: getEnvOrDefault("DOMAIN", "localhost"), 19 | Port: getEnvOrDefault("PORT", ":8080"), 20 | } 21 | if !strings.HasPrefix(settings.Port, ":") { 22 | settings.Port = ":" + settings.Port 23 | } 24 | return &settings 25 | } 26 | 27 | func getEnvOrDefault(key, defaultValue string) string { 28 | value, ok := os.LookupEnv(key) 29 | if !ok { 30 | return defaultValue 31 | } 32 | return value 33 | } 34 | 35 | type AppSettings struct { 36 | Title string 37 | ContactEmail string 38 | Domain string 39 | Port string 40 | } 41 | 42 | func ReadDotenv() { 43 | path := "./.env" 44 | re := regexp.MustCompile(`^[^0-9][A-Z0-9_]+=.+$`) 45 | f, err := os.Open(path) 46 | if err != nil { 47 | log.Fatal("err opening dotenv: ", err) 48 | } 49 | defer f.Close() 50 | 51 | scanner := bufio.NewScanner(f) 52 | for scanner.Scan() { 53 | line := scanner.Bytes() 54 | if len(line) > 0 && line[0] != '#' && re.Match(line) { 55 | split := strings.Split(string(line), "=") 56 | name := strings.TrimSpace(split[0]) 57 | value := strings.TrimSpace(split[1]) 58 | value = strings.Trim(value, `"`) 59 | os.Setenv(name, value) 60 | } else { 61 | slog.Debug("not including invalid or empty line", "line", string(line)) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/views/components/accordion.templ: -------------------------------------------------------------------------------- 1 | // data_display 2 | // https://daisyui.com/components/accordion 3 | package components 4 | 5 | type AccordionRowProps struct { 6 | Label string 7 | Type string 8 | Name string 9 | } 10 | 11 | templ AccordionRow(props AccordionRowProps) { 12 |
13 | 21 |
{ props.Label }
22 |
23 | { children... } 24 |
25 |
26 | } 27 | -------------------------------------------------------------------------------- /internal/views/components/active_search.templ: -------------------------------------------------------------------------------- 1 | // data_display 2 | package components 3 | 4 | import "fmt" 5 | 6 | type ActiveSearchInputProps struct { 7 | ID string 8 | URL string 9 | Target string 10 | InputProps InputProps 11 | } 12 | 13 | templ ActiveSearchInput(props ActiveSearchInputProps) { 14 |
15 |
16 |
25 | @Input(props.InputProps) 26 |
27 | 28 |
29 |
30 |
31 |
32 | } 33 | -------------------------------------------------------------------------------- /internal/views/components/alert.templ: -------------------------------------------------------------------------------- 1 | // feedback 2 | // https://daisyui.com/components/alert 3 | package components 4 | 5 | templ Alert(class string) { 6 | 9 | } 10 | 11 | templ AlertInfo(message string) { 12 | @Alert("alert-info") { 13 | 19 | 25 | 26 | { message } 27 | { children... } 28 | } 29 | } 30 | 31 | templ AlertSuccess(message string) { 32 | @Alert("alert-success") { 33 | 39 | 45 | 46 | { message } 47 | { children... } 48 | } 49 | } 50 | 51 | templ AlertWarning(message string) { 52 | @Alert("alert-warning") { 53 | 59 | 65 | 66 | { message } 67 | { children... } 68 | } 69 | } 70 | 71 | templ AlertError(message string) { 72 | @Alert("alert-error") { 73 | 79 | 85 | 86 | { message } 87 | { children... } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /internal/views/components/anchor.templ: -------------------------------------------------------------------------------- 1 | // navigation 2 | package components 3 | 4 | type AnchorProps struct { 5 | Href string 6 | Label string 7 | LeftIcon templ.Component 8 | RightIcon templ.Component 9 | Attrs templ.Attributes 10 | Class string 11 | } 12 | 13 | templ Anchor(props AnchorProps) { 14 | 21 | if props.LeftIcon != nil { 22 | @props.LeftIcon 23 | } 24 | { props.Label } 25 | if props.RightIcon != nil { 26 | @props.RightIcon 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /internal/views/components/avatar.templ: -------------------------------------------------------------------------------- 1 | // data_display 2 | // https://daisyui.com/components/avatar 3 | package components 4 | 5 | type AvatarProps struct { 6 | AvatarClass string 7 | ContainerClass string 8 | Source string 9 | Placeholder string 10 | PlaceholderClass string 11 | } 12 | 13 | templ Avatar(props AvatarProps) { 14 |
15 |
16 | 17 | if props.Placeholder != "" { 18 | { props.Placeholder } 19 | } 20 |
21 |
22 | } 23 | 24 | templ AvatarGroup(class string) { 25 |
26 | { children... } 27 |
28 | } 29 | -------------------------------------------------------------------------------- /internal/views/components/banner.templ: -------------------------------------------------------------------------------- 1 | // layout 2 | package components 3 | 4 | type BannerProps struct { 5 | Title templ.Component 6 | Description string 7 | } 8 | 9 | templ Banner(props BannerProps) { 10 |
11 |
12 |
13 | @props.Title 14 |

15 | { props.Description } 16 |

17 |
18 | // call to action 19 | { children... } 20 |
21 |
22 |
23 |
24 | } 25 | -------------------------------------------------------------------------------- /internal/views/components/breadcrumbs.templ: -------------------------------------------------------------------------------- 1 | // navigation 2 | // https://daisyui.com/components/breadcrumbs 3 | package components 4 | 5 | type BreadcrumbsProps []BreadcrumbsProp 6 | 7 | type BreadcrumbsProp struct { 8 | Label string 9 | Attrs templ.Attributes 10 | } 11 | 12 | templ Breadcrumbs(props BreadcrumbsProps) { 13 | 26 | } 27 | -------------------------------------------------------------------------------- /internal/views/components/card.templ: -------------------------------------------------------------------------------- 1 | // data_display 2 | // https://daisyui.com/components/card 3 | package components 4 | 5 | type CardProps struct { 6 | Title string 7 | Content string 8 | Source string 9 | Alt string 10 | Class string 11 | } 12 | 13 | templ Card(props CardProps) { 14 |
15 | if props.Source != "" { 16 |
17 | { 18 |
19 | } 20 |
21 |

{ props.Title }

22 |

{ props.Content }

23 |
24 | { children... } 25 |
26 |
27 |
28 | } 29 | -------------------------------------------------------------------------------- /internal/views/components/carousel.templ: -------------------------------------------------------------------------------- 1 | // data_display 2 | // https://daisyui.com/components/carousel 3 | package components 4 | 5 | type CarouselProps []CarouselProp 6 | 7 | type CarouselProp struct { 8 | Source string 9 | Alt string 10 | } 11 | 12 | templ Carousel(props CarouselProps) { 13 | 20 | } 21 | -------------------------------------------------------------------------------- /internal/views/components/chat.templ: -------------------------------------------------------------------------------- 1 | // data_display 2 | // https://daisyui.com/components/chat 3 | package components 4 | 5 | type ChatProps []ChatMessageProps 6 | 7 | type ChatMessageProps struct { 8 | AvatarURL string 9 | Sender string 10 | Time string 11 | Message string 12 | Footer string 13 | Location string 14 | Class string 15 | } 16 | 17 | templ Chat(props ChatProps) { 18 | for _, prop := range props { 19 | @ChatMessage(prop) 20 | } 21 | } 22 | 23 | templ ChatMessage(props ChatMessageProps) { 24 |
31 | if props.AvatarURL != "" { 32 | @Avatar(AvatarProps{ContainerClass: "chat-image w-10 rounded-full", Source: props.AvatarURL}) 33 | } 34 |
35 | { props.Sender } 36 | 37 |
38 |
39 | { props.Message } 40 |
41 | 44 |
45 | } 46 | -------------------------------------------------------------------------------- /internal/views/components/checkbox.templ: -------------------------------------------------------------------------------- 1 | // data_input 2 | // https://daisyui.com/components/checkbox 3 | package components 4 | 5 | type CheckboxProps struct { 6 | ID string 7 | Before string 8 | After string 9 | Name string 10 | Checked bool 11 | Class string 12 | Attrs templ.Attributes 13 | Size string 14 | } 15 | 16 | templ Checkbox(props CheckboxProps) { 17 | 54 | } 55 | -------------------------------------------------------------------------------- /internal/views/components/collapse.templ: -------------------------------------------------------------------------------- 1 | // data_display 2 | // https://daisyui.com/components/collapse 3 | package components 4 | 5 | type CollapseProps struct { 6 | Class string 7 | Title string 8 | TitleClass string 9 | ContentClass string 10 | } 11 | 12 | templ Collapse(props CollapseProps) { 13 |
17 | 18 |
19 | { props.Title } 20 |
21 |
22 | { children... } 23 |
24 |
25 | } 26 | -------------------------------------------------------------------------------- /internal/views/components/combobox.templ: -------------------------------------------------------------------------------- 1 | // data_input 2 | package components 3 | 4 | import ( 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | type ComboboxProps struct { 10 | Label string 11 | Name string 12 | URL string 13 | Options []string 14 | Selected []string 15 | } 16 | 17 | templ Combobox(props ComboboxProps) { 18 | 79 | } 80 | 81 | templ ComboBadge(name, value string) { 82 |
83 | 84 | { value } 85 | 91 | 117 |
118 | } 119 | 120 | templ crossIcon() { 121 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | } 136 | -------------------------------------------------------------------------------- /internal/views/components/countdown.templ: -------------------------------------------------------------------------------- 1 | // data_display 2 | package components 3 | 4 | import ( 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | templ Countdown(expires time.Time) { 10 |
18 | @countDownItem("time().days", "days") 19 | @countDownItem("time().hours", "hours") 20 | @countDownItem("time().minutes", "min") 21 | @countDownItem("time().seconds", "sec") 22 |
23 | 81 | 82 | } 83 | 84 | templ countDownItem(xText, label string) { 85 |
91 |

92 |

{ label }

93 |
94 | } 95 | -------------------------------------------------------------------------------- /internal/views/components/date_picker.templ: -------------------------------------------------------------------------------- 1 | // data_input 2 | package components 3 | 4 | import "fmt" 5 | import "time" 6 | 7 | type DatePickerProps struct { 8 | Year int 9 | Month int 10 | Selected time.Time 11 | StartOfWeek time.Weekday 12 | } 13 | 14 | func (props DatePickerProps) Days() []time.Time { 15 | days := make([]time.Time, 0, 31) 16 | now := time.Now().UTC() 17 | start := time.Date(props.Year, time.Month(props.Month), 1, 0, 0, 0, 0, now.Location()) 18 | end := start.AddDate(0, 1, -1) 19 | for end.Weekday() != props.StartOfWeek { 20 | end = end.AddDate(0, 0, 1) 21 | } 22 | end = end.AddDate(0, 0, -1) 23 | 24 | for start.Weekday() != props.StartOfWeek { 25 | start = start.AddDate(0, 0, -1) 26 | } 27 | for !start.After(end) { 28 | days = append(days, start) 29 | start = start.AddDate(0, 0, 1) 30 | } 31 | return days 32 | } 33 | 34 | func (props DatePickerProps) Months() []time.Time { 35 | months := make([]time.Time, 12) 36 | for i := 1; i <= 12; i++ { 37 | dt := time.Date(props.Year, time.Month(i), 1, 0, 0, 0, 0, time.Now().Location()) 38 | months[i-1] = dt 39 | } 40 | return months 41 | } 42 | 43 | templ DatePicker(props DatePickerProps) { 44 | {{ utcNow := time.Now().UTC() }} 45 | {{ days := props.Days() }} 46 |
50 |
51 | 65 | 88 | 102 |
103 |
104 | if props.StartOfWeek == time.Sunday { 105 | Su 106 | } 107 | Mo 108 | Tu 109 | We 110 | Th 111 | Fr 112 | Sa 113 | if props.StartOfWeek == time.Monday { 114 | Su 115 | } 116 |
117 |
120 | @DatePickerInput(props.Selected) 121 | for _, d := range days { 122 | 143 | } 144 | 162 |
163 |
164 | } 165 | 166 | templ DatePickerInput(d time.Time) { 167 | 168 | } 169 | 170 | templ DatePickerYearPicker(props DatePickerProps) { 171 | {{ utcNow := time.Now().UTC() }} 172 | for i := range 12 { 173 | 180 | } 181 | 188 | } 189 | 190 | templ DatePickerMonthPicker(props DatePickerProps) { 191 | {{ months := props.Months() }} 192 | for _, m := range months { 193 | 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /internal/views/components/diff.templ: -------------------------------------------------------------------------------- 1 | // data_display 2 | // https://daisyui.com/components/diff 3 | package components 4 | 5 | import "fmt" 6 | 7 | type DiffProps struct { 8 | Width int 9 | Height int 10 | Image1 DiffImage 11 | Image2 DiffImage 12 | } 13 | 14 | type DiffImage struct { 15 | Source string 16 | Alt string 17 | } 18 | 19 | templ Diff(props DiffProps) { 20 |
21 |
22 | { 23 |
24 |
25 | { 26 |
27 |
28 |
29 | } 30 | -------------------------------------------------------------------------------- /internal/views/components/drawer.templ: -------------------------------------------------------------------------------- 1 | // layout 2 | // https://daisyui.com/components/drawer 3 | package components 4 | 5 | templ Drawer(toggle templ.Component, sidebar templ.Component) { 6 |
7 | 8 |
9 | 12 | { children... } 13 |
14 |
15 | 16 | 19 |
20 |
21 | } 22 | -------------------------------------------------------------------------------- /internal/views/components/dropdown.templ: -------------------------------------------------------------------------------- 1 | // actions 2 | // https://daisyui.com/components/dropdown 3 | package components 4 | 5 | type DropdownProps struct { 6 | Label string 7 | Class string 8 | ListClass string 9 | Items []DropdownItem 10 | } 11 | 12 | type DropdownItem struct { 13 | Label string 14 | Attrs templ.Attributes 15 | } 16 | 17 | templ Dropdown(props DropdownProps) { 18 |
19 | { props.Label } 20 | 25 |
26 | } 27 | -------------------------------------------------------------------------------- /internal/views/components/features.templ: -------------------------------------------------------------------------------- 1 | // data_display 2 | package components 3 | 4 | type FeaturesProps struct { 5 | Title string 6 | Features []FeatureProps 7 | } 8 | 9 | type FeatureProps struct { 10 | Icon templ.Component 11 | Title string 12 | Description string 13 | URL string 14 | } 15 | 16 | templ Features(props FeaturesProps) { 17 |
18 |
19 | if props.Title != "" { 20 |

{ props.Title }

21 | } 22 |
23 | for _, feature := range props.Features { 24 | @Feature(feature) 25 | } 26 |
27 |
28 |
29 | } 30 | 31 | templ Feature(feature FeatureProps) { 32 |
38 | @feature.Icon 39 | if feature.Title != "" { 40 |

{ feature.Title }

41 | } 42 |

{ feature.Description }

43 | if feature.URL != "" { 44 | Learn more 45 | } 46 |
47 | } 48 | -------------------------------------------------------------------------------- /internal/views/components/file_input.templ: -------------------------------------------------------------------------------- 1 | // data_input 2 | // https://daisyui.com/components/file-input 3 | package components 4 | 5 | type FileInputProps struct { 6 | Name string 7 | Label string 8 | Value string 9 | Description string 10 | Attrs templ.Attributes 11 | DisabledMessage string 12 | Required bool 13 | Size string 14 | } 15 | 16 | templ FileInput(props FileInputProps) { 17 |
23 | if props.Label != "" { 24 | { props.Label } 25 | } 26 | 41 | if props.Description != "" { 42 | 43 | } 44 |
45 | } 46 | -------------------------------------------------------------------------------- /internal/views/components/footer.templ: -------------------------------------------------------------------------------- 1 | // layout 2 | // https://daisyui.com/components/footer 3 | package components 4 | 5 | type FooterProps struct { 6 | Icon templ.Component 7 | Name string 8 | Description string 9 | Copyright string 10 | Anchors []AnchorProps 11 | } 12 | 13 | templ Footer(props FooterProps) { 14 | 17 | 41 | } 42 | 43 | templ FooterNav(title string, links []AnchorProps) { 44 | 50 | } 51 | -------------------------------------------------------------------------------- /internal/views/components/hero.templ: -------------------------------------------------------------------------------- 1 | // layout 2 | // https://daisyui.com/components/hero 3 | package components 4 | 5 | type HeroProps struct { 6 | Source string 7 | Alt string 8 | Reverse bool 9 | Class string 10 | } 11 | 12 | templ Hero(props HeroProps) { 13 |
14 |
22 | if props.Source != "" { 23 | { 28 | } 29 |
30 | { children... } 31 |
32 |
33 |
34 | } 35 | -------------------------------------------------------------------------------- /internal/views/components/infinite_scroll.templ: -------------------------------------------------------------------------------- 1 | // data_display 2 | package components 3 | 4 | import "fmt" 5 | 6 | templ InfiniteScrollTable(rows []templ.Component) { 7 | @Table( 8 | []templ.Component{PlainText("Name"), PlainText("Email")}, 9 | rows, 10 | nil, 11 | ) 12 |
13 | } 14 | 15 | templ InfiniteScrollRows(rows []templ.Component) { 16 | for _, r := range rows { 17 | @r 18 | } 19 | } 20 | 21 | templ InfiniteScrollRow(name, email string, page int, hasMore bool) { 22 | 31 | { name } 32 | { email } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /internal/views/components/input.templ: -------------------------------------------------------------------------------- 1 | // data_input 2 | package components 3 | 4 | type InputProps struct { 5 | // common 6 | Name string 7 | Label string 8 | Type string // defaults to "text" 9 | Value string 10 | Placeholder string 11 | Error string 12 | DisabledMessage string 13 | Size string // xs sm lg xl, default: md 14 | Required bool 15 | Icon templ.Component 16 | Attrs templ.Attributes 17 | 18 | // text input 19 | MinLength string 20 | MaxLength string 21 | Pattern string 22 | ValidatorHint string 23 | 24 | // integer/decimal input 25 | Min string 26 | Max string 27 | 28 | // decimal input 29 | Step string 30 | } 31 | 32 | templ Input(props InputProps) { 33 |
34 | if props.Label != "" { 35 | { props.Label } 36 | } 37 | 78 | if props.Error != "" { 79 |

{ props.Error }

80 | } 81 | if props.ValidatorHint != "" { 82 | 83 | } 84 |
85 | } 86 | -------------------------------------------------------------------------------- /internal/views/components/lazy_load.templ: -------------------------------------------------------------------------------- 1 | // data_display 2 | package components 3 | 4 | templ LazyLoad(url string) { 5 |
11 | 12 |
13 | } 14 | -------------------------------------------------------------------------------- /internal/views/components/menu.templ: -------------------------------------------------------------------------------- 1 | // navigation 2 | // https://daisyui.com/components/menu 3 | package components 4 | 5 | type MenuProps struct { 6 | Title string 7 | Class string 8 | } 9 | 10 | templ Menu(props MenuProps) { 11 | 17 | } 18 | 19 | type MenuItemProps struct { 20 | Label string 21 | Attrs templ.Attributes 22 | Icon templ.Component 23 | IconAfter bool 24 | } 25 | 26 | templ MenuItem(props MenuItemProps) { 27 |
  • 28 | 29 | if props.Icon != nil && !props.IconAfter { 30 | @props.Icon 31 | } 32 | { props.Label } 33 | if props.Icon != nil && props.IconAfter { 34 | @props.Icon 35 | } 36 | 37 | 40 |
  • 41 | } 42 | 43 | type SubmenuProps struct { 44 | Title string 45 | Attrs templ.Attributes 46 | Icon templ.Component 47 | IconAfter bool 48 | } 49 | 50 | templ Submenu(props SubmenuProps) { 51 |
  • 52 |
    53 | 54 | if props.Icon != nil && !props.IconAfter { 55 | @props.Icon 56 | } 57 | { props.Title } 58 | if props.Icon != nil && props.IconAfter { 59 | @props.Icon 60 | } 61 | 62 |
      63 | { children... } 64 |
    65 |
    66 |
  • 67 | } 68 | -------------------------------------------------------------------------------- /internal/views/components/modal.templ: -------------------------------------------------------------------------------- 1 | // actions 2 | // https://daisyui.com/components/modal 3 | package components 4 | 5 | import "fmt" 6 | 7 | type ModalProps struct { 8 | ID string 9 | Label any 10 | } 11 | 12 | templ Modal(props ModalProps) { 13 | @modalWrapper( 14 | props, 15 | templ.Attributes{"onclick": fmt.Sprintf("%s.showModal()", props.ID)}, 16 | ) { 17 | { children... } 18 | } 19 | } 20 | 21 | templ modalWrapper(props ModalProps, attrs templ.Attributes) { 22 | // you can use a string or a templ.Component as the 'label' 23 | // of the modal button 24 | if s, ok := props.Label.(string); ok { 25 |
    26 | { s } 27 |
    28 | } else if c, ok := props.Label.(templ.Component); ok { 29 | @c 30 | } 31 | 32 | 35 | 38 | 39 | } 40 | -------------------------------------------------------------------------------- /internal/views/components/pagination.templ: -------------------------------------------------------------------------------- 1 | // navigation 2 | // https://daisyui.com/components/pagination 3 | package components 4 | 5 | import "fmt" 6 | 7 | type PaginationProps struct { 8 | URL string 9 | Page int 10 | Low int 11 | High int 12 | MaxPages int 13 | } 14 | 15 | templ Pagination(id string, props PaginationProps) { 16 |
    17 | 18 | { children... } 19 | 20 |
    21 | @PaginationButton(id, props.URL, 1, props.Page == 1) { 22 | @AnglesLeft() 23 | } 24 | @PaginationButton(id, props.URL, props.Page-1, props.Page == 1) { 25 | @ChevronLeft() 26 | } 27 | for i := props.Low; i <= props.High; i++ { 28 | @PaginationButton(id, props.URL, i+1, props.Page == i+1) { 29 | { fmt.Sprintf("%d", i+1) } 30 | } 31 | } 32 | @PaginationButton(id, props.URL, props.Page+1, props.Page == props.MaxPages) { 33 | @ChevronRight() 34 | } 35 | @PaginationButton(id, props.URL, props.MaxPages, props.Page == props.MaxPages) { 36 | @AnglesRight() 37 | } 38 |
    39 |
    40 | } 41 | 42 | templ PaginationButton(id, url string, urlPage int, disabled bool) { 43 | 59 | } 60 | 61 | templ AnglesRight() { 62 | 68 | 75 | 76 | } 77 | 78 | templ AnglesLeft() { 79 | 85 | 92 | 93 | } 94 | 95 | templ ChevronRight() { 96 | 102 | 109 | 110 | } 111 | 112 | templ ChevronLeft() { 113 | 119 | 126 | 127 | } 128 | -------------------------------------------------------------------------------- /internal/views/components/pricing.templ: -------------------------------------------------------------------------------- 1 | // data_display 2 | package components 3 | 4 | import "slices" 5 | 6 | type PricingProps struct { 7 | Checked bool 8 | Prices []PriceProps 9 | } 10 | 11 | type PriceProps struct { 12 | Title string 13 | Description string 14 | PriceMonthly string 15 | PerMonthly string 16 | PriceAnnually string 17 | PerAnnually string 18 | PerUser bool 19 | Promotion string 20 | IncludedFeatures []string 21 | ExcludedFeatures []string 22 | CallToAction PriceButtonProps 23 | Footer templ.Component 24 | } 25 | 26 | type PriceButtonProps struct { 27 | Label string 28 | Attrs templ.Attributes 29 | } 30 | 31 | // NOTE: Requires Alpine.js 32 | templ Pricing(props PricingProps) { 33 | 34 |
    35 |
    36 | @Toggle(ToggleProps{ 37 | Before: "Billed monthly", 38 | After: "Billed annually", 39 | Name: "period", 40 | Checked: props.Checked, 41 | Highlight: true, 42 | Attrs: templ.Attributes{ 43 | "x-on:click": "yearly = !yearly", 44 | }, 45 | }) 46 |
    47 | @PriceGrid(props.Prices) 48 |
    49 | } 50 | 51 | templ PriceGrid(prices []PriceProps) { 52 |
    59 | for i := range prices { 60 | @Price(prices[i], nil) 61 | } 62 |
    63 | } 64 | 65 | templ Price(price PriceProps, footer templ.Component) { 66 |
    67 |
    75 | if price.Promotion != "" { 76 | 77 | { price.Promotion } 78 | 79 | } 80 |
    81 |

    { price.Title }

    82 |
    83 |

    84 | { price.PriceAnnually } 85 | 86 | { price.PerAnnually } 87 | 88 | if price.PerUser { 89 | 90 | { " / user" } 91 | 92 | } 93 |

    94 |

    95 | { price.PriceMonthly } { price.PerMonthly } 96 |

    97 | 100 |
    101 |
      102 | for i := range price.IncludedFeatures { 103 |
    • 104 | 105 | 106 | 107 | { price.IncludedFeatures[i] } 108 |
    • 109 | } 110 |
    111 | if len(price.ExcludedFeatures) > 0 { 112 |
    113 | } 114 |
      115 | for i := range price.ExcludedFeatures { 116 |
    • 117 | { price.ExcludedFeatures[i] } 118 |
    • 119 | } 120 |
    121 |
    122 | if footer != nil { 123 | @footer 124 | } 125 |
    126 |
    127 | } 128 | -------------------------------------------------------------------------------- /internal/views/components/radio.templ: -------------------------------------------------------------------------------- 1 | // data_input 2 | // https://daisyui.com/components/radio 3 | package components 4 | 5 | type RadioProps struct { 6 | Name string 7 | Values map[string]string 8 | Class string 9 | Size string 10 | } 11 | 12 | templ Radio(props RadioProps) { 13 |
    14 | for l, v := range props.Values { 15 | 38 | } 39 |
    40 | } 41 | -------------------------------------------------------------------------------- /internal/views/components/range.templ: -------------------------------------------------------------------------------- 1 | // data_input 2 | // https://daisyui.com/components/range 3 | package components 4 | 5 | import "fmt" 6 | 7 | type RangeProps struct { 8 | ID string 9 | Label string 10 | Name string 11 | Value int 12 | Min int 13 | Max int 14 | Step int 15 | Class string 16 | Size string 17 | } 18 | 19 | // Note: usage requires alpine.js 20 | templ Range(props RangeProps) { 21 | 22 |
    23 | 57 |
    58 | } 59 | 60 | // Note: usage requires datastar.js 61 | templ DatastarRange(props RangeProps) { 62 | 63 |
    64 | 101 |
    102 | } 103 | -------------------------------------------------------------------------------- /internal/views/components/rating.templ: -------------------------------------------------------------------------------- 1 | // data_input 2 | // https://daisyui.com/components/rating 3 | package components 4 | 5 | import "fmt" 6 | 7 | type RatingProps struct { 8 | Name string 9 | Min int 10 | Max int 11 | Class string 12 | Value int 13 | } 14 | 15 | templ Rating(props RatingProps) { 16 |
    17 | for i := props.Min; i <= props.Max; i++ { 18 | if i == 0 { 19 | 25 | } else { 26 | 35 | } 36 | } 37 |
    38 | } 39 | 40 | templ RatingDisplay(props RatingProps) { 41 |
    42 | for i := props.Min; i <= props.Max; i++ { 43 | if i == 0 { 44 | 51 | } else { 52 | 62 | } 63 | } 64 |
    65 | } 66 | -------------------------------------------------------------------------------- /internal/views/components/select.templ: -------------------------------------------------------------------------------- 1 | // data_input 2 | // https://daisyui.com/components/select 3 | package components 4 | 5 | type SelectProps struct { 6 | ID string 7 | Label string 8 | Name string 9 | Options []SelectOption 10 | Attrs templ.Attributes 11 | Class string 12 | Size string 13 | } 14 | 15 | type SelectOption struct { 16 | Label string 17 | Value string 18 | Selected bool 19 | Disabled bool 20 | } 21 | 22 | templ Select(props SelectProps) { 23 | 55 | } 56 | 57 | templ SelectOptions(options []SelectOption) { 58 | for i := range options { 59 | 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /internal/views/components/skeleton.templ: -------------------------------------------------------------------------------- 1 | // feedback 2 | // https://daisyui.com/components/skeleton 3 | package components 4 | 5 | templ Skeleton() { 6 |
    7 |
    8 |
    9 |
    10 |
    11 |
    12 | } 13 | -------------------------------------------------------------------------------- /internal/views/components/stats.templ: -------------------------------------------------------------------------------- 1 | // data_display 2 | // https://daisyui.com/components/stat 3 | package components 4 | 5 | type StatProps struct { 6 | Title string 7 | Value string 8 | Description string 9 | } 10 | 11 | templ Stats() { 12 |
    13 | { children... } 14 |
    15 | } 16 | 17 | templ Stat(props StatProps) { 18 |
    19 |
    { props.Title }
    20 |
    { props.Value }
    21 | if props.Description != "" { 22 |
    { props.Description }
    23 | } 24 |
    25 | { children... } 26 |
    27 |
    28 | } 29 | -------------------------------------------------------------------------------- /internal/views/components/status.templ: -------------------------------------------------------------------------------- 1 | // feedback 2 | package components 3 | 4 | import "fmt" 5 | 6 | type StatusProps struct { 7 | Code int 8 | Title string 9 | Description string 10 | ReturnButtonLabel string 11 | ReturnButtonAttrs templ.Attributes 12 | } 13 | 14 | templ Status(props StatusProps) { 15 |
    16 |
    17 |

    18 | { fmt.Sprintf("%d", props.Code) } 19 |

    20 |

    21 | { props.Title } 22 |

    23 |

    24 | { props.Description } 25 |

    26 | 30 | { props.ReturnButtonLabel } 31 | 32 |
    33 |
    34 | } 35 | -------------------------------------------------------------------------------- /internal/views/components/steps.templ: -------------------------------------------------------------------------------- 1 | // navigation 2 | // https://daisyui.com/components/steps 3 | package components 4 | 5 | type StepProps struct { 6 | Label string 7 | Done bool 8 | Attrs templ.Attributes 9 | } 10 | 11 | templ Steps() { 12 | 15 | } 16 | 17 | templ Step(props StepProps) { 18 |
  • 21 | { props.Label } 22 |
  • 23 | } 24 | -------------------------------------------------------------------------------- /internal/views/components/swap.templ: -------------------------------------------------------------------------------- 1 | // actions 2 | // https://daisyui.com/components/swap 3 | package components 4 | 5 | type SwapProps struct { 6 | On templ.Component 7 | Off templ.Component 8 | Class string 9 | } 10 | 11 | templ Swap(props SwapProps) { 12 | 21 | } 22 | -------------------------------------------------------------------------------- /internal/views/components/table.templ: -------------------------------------------------------------------------------- 1 | // data_display 2 | // https://daisyui.com/components/table 3 | package components 4 | 5 | templ Table(headers []templ.Component, rows []templ.Component, attrs templ.Attributes) { 6 |
    7 | 8 | 9 | 10 | for _, header := range headers { 11 | 14 | } 15 | 16 | 17 | 18 | for _, trow := range rows { 19 | @trow 20 | } 21 | 22 |
    12 | @header 13 |
    23 |
    24 | } 25 | 26 | // Component to use as plain text when 27 | // templ.Component is used as argument 28 | templ PlainText(content string) { 29 | { content } 30 | } 31 | -------------------------------------------------------------------------------- /internal/views/components/tabs.templ: -------------------------------------------------------------------------------- 1 | // navigation 2 | // https://daisyui.com/components/tab 3 | package components 4 | 5 | type TabsProps struct { 6 | Name string 7 | Class string 8 | Tabs []TabProps 9 | ContentClass string 10 | } 11 | 12 | type TabProps struct { 13 | Label string 14 | Content templ.Component 15 | } 16 | 17 | templ Tabs(props TabsProps) { 18 |
    19 | for i, tab := range props.Tabs { 20 | 30 |
    31 | @tab.Content 32 |
    33 | } 34 |
    35 | } 36 | -------------------------------------------------------------------------------- /internal/views/components/testimonial.templ: -------------------------------------------------------------------------------- 1 | // data_display 2 | package components 3 | 4 | import "fmt" 5 | 6 | type TestimonialProps []TestimonialProp 7 | 8 | type TestimonialProp struct { 9 | Avatar templ.Component 10 | Name string 11 | Rating int 12 | Content string 13 | } 14 | 15 | templ TestimonialGrid(title string, props TestimonialProps) { 16 |
    17 |
    18 |

    19 | { title } 20 |

    21 |
    22 | for i := range props { 23 |
    24 |
    25 |
    26 | if props[i].Avatar != nil { 27 | @props[i].Avatar 28 | } 29 |
    30 | @RatingDisplay(RatingProps{ 31 | Name: fmt.Sprintf("review-rating-%d", i), 32 | Min: 1, 33 | Max: 5, 34 | Value: props[i].Rating, 35 | }) 36 |

    { props[i].Name }

    37 |
    38 |
    39 |

    40 | { props[i].Content } 41 |

    42 |
    43 |
    44 | } 45 |
    46 |
    47 |
    48 | } 49 | -------------------------------------------------------------------------------- /internal/views/components/textarea.templ: -------------------------------------------------------------------------------- 1 | // data_input 2 | // https://daisyui.com/components/textarea 3 | package components 4 | 5 | import "fmt" 6 | 7 | type TextareaProps struct { 8 | ID string 9 | Label string 10 | Name string 11 | Placeholder string 12 | Value string 13 | Rows int 14 | Err string 15 | Class string 16 | Attrs templ.Attributes 17 | Size string 18 | } 19 | 20 | templ Textarea(props TextareaProps) { 21 |
    22 | if props.Label != "" { 23 | { props.Label } 24 | } 25 | 49 |
    { props.Err }
    50 |
    51 | } 52 | -------------------------------------------------------------------------------- /internal/views/components/time_slot_picker.templ: -------------------------------------------------------------------------------- 1 | // data_input 2 | package components 3 | 4 | import "time" 5 | 6 | type TimeSlotPickerProps struct { 7 | ID string 8 | CurrentDate time.Time 9 | TimeSlots []TimeSlot 10 | PickerURL string 11 | ReserveURL string 12 | } 13 | 14 | type TimeSlot struct { 15 | Start time.Time 16 | End time.Time 17 | } 18 | 19 | func (x *TimeSlotPickerProps) Days() []time.Time { 20 | date := time.Date(x.CurrentDate.Year(), x.CurrentDate.Month(), x.CurrentDate.Day(), 0, 0, 0, 0, x.CurrentDate.Location()) 21 | dates := make([]time.Time, 7) 22 | dates[0] = date 23 | for i := range 6 { 24 | date = date.Add(24 * time.Hour) 25 | dates[i+1] = date 26 | } 27 | return dates 28 | } 29 | 30 | func (x *TimeSlotPickerProps) GetSlots(day time.Time) []TimeSlot { 31 | slots := make([]TimeSlot, 0) 32 | now := time.Now().UTC() 33 | now = time.Date( 34 | now.Year(), now.Month(), now.Day(), 35 | now.Hour(), 0, 0, 0, 36 | now.Location()).Add(time.Duration(24-now.Hour()) * time.Hour) 37 | for _, s := range x.TimeSlots { 38 | if s.Start.After(now) && s.Start.Format("20060102") == day.Format("20060102") { 39 | slots = append(slots, s) 40 | } 41 | } 42 | return slots 43 | } 44 | 45 | templ TimeSlotPicker(props TimeSlotPickerProps) { 46 |
    47 |
    48 | for _, day := range props.Days() { 49 | @timeSlotPickerDay(props, day) 50 | } 51 |
    52 | @timeSlotPickerControls(props) 53 |
    54 | } 55 | 56 | templ timeSlotPickerDay(props TimeSlotPickerProps, day time.Time) { 57 |
    63 |
    64 | { day.Format("Mon Jan 02") } 65 |
    66 |
    67 | 82 |
    83 | } 84 | 85 | templ timeSlotButton(slot TimeSlot, attrs templ.Attributes) { 86 | 89 | } 90 | 91 | templ timeSlotModalContent(slot TimeSlot, slotName, reserveURL string) { 92 |
    93 |
    94 |
    95 | 101 | 102 | 103 | 104 | 105 |
    106 |

    107 | Reserve a time slot { slot.Start.Format("15:04") } - { slot.End.Format("15:04") }, { slot.Start.Format("Monday January 02") }? 108 |

    109 |
    110 |
    111 | 118 | 121 |
    122 | 131 |
    132 | } 133 | 134 | templ timeSlotPickerControls(props TimeSlotPickerProps) { 135 |
    136 | 147 | 155 |
    156 | } 157 | 158 | templ chevronLeft() { 159 | 165 | 166 | 167 | } 168 | 169 | templ chevronRight() { 170 | 176 | 177 | 178 | } 179 | -------------------------------------------------------------------------------- /internal/views/components/timeline.templ: -------------------------------------------------------------------------------- 1 | // data_display 2 | // https://daisyui.com/components/timeline 3 | package components 4 | 5 | type TimelineProps []TimelineProp 6 | 7 | type TimelineProp struct { 8 | Start string 9 | Middle templ.Component 10 | End string 11 | } 12 | 13 | templ Timeline(props TimelineProps) { 14 | 37 | } 38 | 39 | templ TimelineCheckbox(checked bool) { 40 | 46 | 51 | 52 | } 53 | -------------------------------------------------------------------------------- /internal/views/components/toast.templ: -------------------------------------------------------------------------------- 1 | // feedback 2 | // https://daisyui.com/components/toast 3 | package components 4 | 5 | type ToastProps struct { 6 | Name string 7 | ToastClass string 8 | AlertClass string 9 | } 10 | 11 | templ Toast(props ToastProps) { 12 |
    13 |
    14 | { children... } 15 |
    16 |
    17 | } 18 | -------------------------------------------------------------------------------- /internal/views/components/toggle.templ: -------------------------------------------------------------------------------- 1 | // data_input 2 | // https://daisyui.com/components/toggle 3 | package components 4 | 5 | type ToggleProps struct { 6 | ID string 7 | Before string 8 | After string 9 | Name string 10 | Checked bool 11 | Class string 12 | Highlight bool 13 | Attrs templ.Attributes 14 | Size string 15 | } 16 | 17 | templ Toggle(props ToggleProps) { 18 |
    19 | 65 | if props.Highlight { 66 | 72 | } 73 |
    74 | } 75 | -------------------------------------------------------------------------------- /internal/views/components/tooltip.templ: -------------------------------------------------------------------------------- 1 | // feedback 2 | // https://daisyui.com/components/tooltip 3 | package components 4 | 5 | type TooltipProps struct { 6 | Tip string 7 | Class string 8 | } 9 | 10 | templ Tooltip(props TooltipProps) { 11 |
    12 | { children... } 13 |
    14 | } 15 | -------------------------------------------------------------------------------- /internal/views/custom/toast.templ: -------------------------------------------------------------------------------- 1 | package custom 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | templ ToastErrorConfirm(errs ...string) { 6 | @components.Toast( 7 | components.ToastProps{ 8 | Name: "toast-error", 9 | ToastClass: "toast-top toast-center", 10 | AlertClass: "alert-error", 11 | }, 12 | ) { 13 | 19 | 20 | 21 |
    22 | 27 |
    28 | 29 | 34 | } 35 | } 36 | 37 | templ HXToastInfoFade(messages ...string) { 38 | @components.Toast( 39 | components.ToastProps{ 40 | Name: "toast-info", 41 | ToastClass: "toast-bottom toast-end w-full max-w-xs", 42 | AlertClass: "alert-info w-full max-w-xs space-x-4", 43 | }, 44 | ) { 45 | 46 | 47 | 48 | 49 | 50 |
    51 | 58 |
    59 | 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/views/examples/accordion.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | /* 7 | Accordion with input type 'checkbox': multiple rows can be open at a time. 8 | */ 9 | 10 | templ AccordionWithCheckbox() { 11 |
    12 | @components.AccordionRow(components.AccordionRowProps{Label: "Label 1", Name: "accordion-example-1"}) { 13 |

    Label 1 content

    14 | } 15 | @components.AccordionRow(components.AccordionRowProps{Label: "Label 2", Name: "accordion-example-2"}) { 16 |

    Content 2

    17 |

    Label 2 content

    18 | } 19 | @components.AccordionRow(components.AccordionRowProps{Label: "Label 3", Name: "accordion-example-3"}) { 20 |

    Content 3

    21 | 26 | } 27 |
    28 | } 29 | 30 | // example 31 | /* 32 | Accordion with input type 'radio': only a single row can be open at a time. 33 | */ 34 | 35 | templ AccordionWithRadio() { 36 |
    37 | @components.AccordionRow(components.AccordionRowProps{Label: "Label 1", Name: "accordion-example-2", Type: "radio"}) { 38 |

    Label 1 content

    39 | } 40 | @components.AccordionRow(components.AccordionRowProps{Label: "Label 2", Name: "accordion-example-2", Type: "radio"}) { 41 |

    Content 2

    42 |

    Label 2 content

    43 | } 44 | @components.AccordionRow(components.AccordionRowProps{Label: "Label 3", Name: "accordion-example-2", Type: "radio"}) { 45 |

    Content 3

    46 | 51 | } 52 |
    53 | } 54 | -------------------------------------------------------------------------------- /internal/views/examples/active_search.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "fmt" 5 | "github.com/haatos/goshipit/internal/views/components" 6 | ) 7 | 8 | // example 9 | // Active search input for a table 10 | templ ActiveSearchExampleTable() { 11 |
    12 | @ActiveSearchExample( 13 | "active-search-example-table", 14 | []templ.Component{ 15 | components.PlainText("First name"), 16 | components.PlainText("Last name"), 17 | components.PlainText("Email"), 18 | }, 19 | activeSearchTableDataComponents(), 20 | ) 21 |
    22 | } 23 | 24 | templ ActiveSearchExample(id string, headers []templ.Component, rows []templ.Component) { 25 |
    26 | @components.ActiveSearchInput( 27 | components.ActiveSearchInputProps{ 28 | ID: "active-search-example-input", 29 | URL: "/active-search", 30 | Target: fmt.Sprintf("#%s > tbody", id), 31 | InputProps: components.InputProps{ 32 | Icon: searchIcon(), 33 | Name: "active-search-example", 34 | Type: "search", 35 | Placeholder: "Filter table...", 36 | Size: "sm", 37 | }, 38 | }) 39 | @components.Table( 40 | headers, 41 | activeSearchTableDataComponents(), 42 | templ.Attributes{"id": "active-search-example-table"}, 43 | ) 44 |
    45 | } 46 | 47 | templ searchIcon() { 48 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | } 63 | 64 | func activeSearchTableDataComponents() []templ.Component { 65 | coms := make([]templ.Component, len(ActiveSearchTableData)) 66 | for i := range ActiveSearchTableData { 67 | coms[i] = ActiveSearchTableRow( 68 | ActiveSearchTableData[i].FirstName, 69 | ActiveSearchTableData[i].LastName, 70 | ActiveSearchTableData[i].Email) 71 | } 72 | return coms 73 | } 74 | 75 | var ActiveSearchTableData = []struct { 76 | FirstName string 77 | LastName string 78 | Email string 79 | }{ 80 | {FirstName: "John", LastName: "Smith", Email: "john.smith@email.com"}, 81 | {FirstName: "Emily", LastName: "Johnson", Email: "emily.johnson@email.com"}, 82 | {FirstName: "Michael", LastName: "Brown", Email: "michael.brown@email.com"}, 83 | {FirstName: "Jessica", LastName: "Williams", Email: "jessica.williams@email.com"}, 84 | {FirstName: "David", LastName: "Jones", Email: "david.jones@email.com"}, 85 | {FirstName: "Sarah", LastName: "Miller", Email: "sarah.miller@email.com"}, 86 | {FirstName: "Christopher", LastName: "Davis", Email: "chris.davis@email.com"}, 87 | {FirstName: "Amanda", LastName: "Wilson", Email: "amanda.wilson@email.com"}, 88 | {FirstName: "James", LastName: "Taylor", Email: "james.taylor@email.com"}, 89 | {FirstName: "Laura", LastName: "Moore", Email: "laura.moore@email.com"}, 90 | } 91 | 92 | templ ActiveSearchTableRows(rows []templ.Component) { 93 | for _, r := range rows { 94 | @r 95 | } 96 | } 97 | 98 | templ ActiveSearchTableRow(firstName, lastName, email string) { 99 | 100 | { firstName } 101 | { lastName } 102 | { email } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /internal/views/examples/alert.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | // Info-type alert 7 | templ AlertInfoExample() { 8 |
    9 | @components.AlertInfo( 10 | "Your profile has been successfully updated. Please review your changes.", 11 | ) 12 |
    13 | } 14 | 15 | // example 16 | // Success-type alert 17 | templ AlertSuccessExample() { 18 |
    19 | @components.AlertSuccess( 20 | "Your payment was processed successfully! Thank you for your purchase.", 21 | ) { 22 | 23 | } 24 |
    25 | } 26 | 27 | // example 28 | // Warning-type alert 29 | templ AlertWarningExample() { 30 |
    31 | @components.AlertWarning( 32 | "Your password will expire in 7 days. Please update it to avoid any disruptions.", 33 | ) 34 |
    35 | } 36 | 37 | // example 38 | // Error-type alert 39 | templ AlertErrorExample() { 40 |
    41 | @components.AlertError( 42 | "Failed to connect to the server. Please try again later or contact support if the issue persists.", 43 | ) { 44 | 45 | } 46 |
    47 | } 48 | -------------------------------------------------------------------------------- /internal/views/examples/anchor.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | // Basic anchor 7 | templ BasicAnchor() { 8 | @components.Anchor(components.AnchorProps{ 9 | Label: "Basic anchor", 10 | Class: "link", 11 | }) 12 | } 13 | 14 | // example 15 | // Primary anchor 16 | templ PrimaryAnchor() { 17 | @components.Anchor(components.AnchorProps{ 18 | Label: "Primary anchor", 19 | Class: "link link-primary", 20 | }) 21 | } 22 | 23 | // example 24 | // Anchor with icon 25 | templ AnchorWithIcon() { 26 | @components.Anchor(components.AnchorProps{ 27 | Label: "GitHub profile", 28 | LeftIcon: GithubIcon(), 29 | Class: "link", 30 | }) 31 | } 32 | 33 | // example 34 | // Socials anchors 35 | templ SocialAnchors() { 36 |
    37 |
    38 | @components.Anchor(components.AnchorProps{LeftIcon: XIcon()}) 39 | @components.Anchor(components.AnchorProps{LeftIcon: YoutubeIcon()}) 40 | @components.Anchor(components.AnchorProps{LeftIcon: FacebookIcon()}) 41 | @components.Anchor(components.AnchorProps{LeftIcon: GithubIcon()}) 42 | @components.Anchor(components.AnchorProps{LeftIcon: LinkedInIcon()}) 43 |
    44 |
    45 | } 46 | 47 | templ XIcon() { 48 | 55 | 56 | 57 | } 58 | 59 | templ YoutubeIcon() { 60 | 65 | 68 | 69 | } 70 | 71 | templ FacebookIcon() { 72 | 77 | 80 | 81 | } 82 | 83 | templ GithubIcon() { 84 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | } 100 | 101 | templ LinkedInIcon() { 102 | 108 | 109 | 110 | } 111 | -------------------------------------------------------------------------------- /internal/views/examples/avatar.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | // Multiple avatar sizes 7 | templ MultipleAvatarSizes() { 8 |
    9 | @components.Avatar(components.AvatarProps{ 10 | ContainerClass: "rounded w-8", Source: "/static/images/avatar.jpg"}) 11 | @components.Avatar(components.AvatarProps{ 12 | ContainerClass: "rounded w-12", Source: "/static/images/avatar.jpg"}) 13 | @components.Avatar(components.AvatarProps{ 14 | ContainerClass: "rounded w-16", Source: "/static/images/avatar.jpg"}) 15 | @components.Avatar(components.AvatarProps{ 16 | ContainerClass: "rounded w-20", Source: "/static/images/avatar.jpg"}) 17 | @components.Avatar(components.AvatarProps{ 18 | ContainerClass: "rounded w-24", Source: "/static/images/avatar.jpg"}) 19 |
    20 | } 21 | 22 | // example 23 | // Avatar group 24 | templ GroupOfAvatars() { 25 |
    26 | @components.AvatarGroup("-space-x-8") { 27 | @components.Avatar(components.AvatarProps{ 28 | ContainerClass: "rounded-full w-12", Source: "/static/images/avatar.jpg"}) 29 | @components.Avatar(components.AvatarProps{ 30 | ContainerClass: "rounded-full w-12", Source: "/static/images/avatar.jpg"}) 31 | @components.Avatar(components.AvatarProps{ 32 | ContainerClass: "rounded-full w-12", Source: "/static/images/avatar.jpg"}) 33 | @components.Avatar(components.AvatarProps{ 34 | ContainerClass: "rounded-full w-12", Source: "/static/images/avatar.jpg"}) 35 | @components.Avatar(components.AvatarProps{ 36 | ContainerClass: "rounded-full w-12", Source: "/static/images/avatar.jpg"}) 37 | } 38 |
    39 | } 40 | 41 | // example 42 | // Avatar with online/offline indicator 43 | templ OnlineAndOffline() { 44 |
    45 | @components.Avatar(components.AvatarProps{ 46 | AvatarClass: "avatar-online", 47 | ContainerClass: "rounded-full w-12", 48 | Source: "/static/images/avatar.jpg", 49 | }) 50 | @components.Avatar(components.AvatarProps{ 51 | AvatarClass: "avatar-offline", 52 | ContainerClass: "rounded-full w-12", 53 | Source: "/static/images/avatar.jpg", 54 | }) 55 |
    56 | } 57 | -------------------------------------------------------------------------------- /internal/views/examples/banner.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | templ BannerExample() { 7 | @components.Banner(components.BannerProps{ 8 | Title: basicBannerTitle(), 9 | Description: `Lorem ipsum dolor sit amet consectetur adipisicing elit. 10 | Sapiente iure non, quia perspiciatis sed temporibus quos nihil, voluptatibus tempore 11 | placeat ipsa est facilis, nobis illum in magni illo neque libero.`, 12 | }) { 13 | 14 | Get started 15 | 16 | 17 | Learn more 18 | 19 | } 20 | } 21 | 22 | templ basicBannerTitle() { 23 |

    24 | Lorem ipsum dolor. 25 | Sit amet consectetur. 26 |

    27 | } 28 | -------------------------------------------------------------------------------- /internal/views/examples/breadcrumbs.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | templ BreadcrumbsExample() { 7 |
    8 | @components.Breadcrumbs( 9 | components.BreadcrumbsProps{ 10 | { 11 | Label: "Laptops", 12 | Attrs: templ.Attributes{"href": "/laptops", "class": "link"}, 13 | }, 14 | { 15 | Label: "Macbooks", 16 | Attrs: templ.Attributes{"href": "/laptops/macbooks", "class": "link"}, 17 | }, 18 | {Label: "Macbook Pro 14"}, 19 | }, 20 | ) 21 |
    22 | } 23 | -------------------------------------------------------------------------------- /internal/views/examples/card.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | // Basic card 7 | templ BasicCard() { 8 |
    9 | @components.Card( 10 | components.CardProps{ 11 | Title: "This is a card", 12 | Content: "And this is the card's content.", 13 | Class: "bg-base-100 w-96 shadow-xl", 14 | }, 15 | ) 16 |
    17 | } 18 | 19 | // example 20 | // Card with image 21 | templ BasicCardWithImage() { 22 |
    23 | @components.Card( 24 | components.CardProps{ 25 | Title: "Card with image", 26 | Content: "Card with image content", 27 | Source: "/static/images/avatar.jpg", 28 | Alt: "avatar image", 29 | Class: "card-bordered bg-base-100 w-96 shadow-xl", 30 | }, 31 | ) { 32 | 33 | } 34 |
    35 | } 36 | -------------------------------------------------------------------------------- /internal/views/examples/carousel.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | templ CarouselExample() { 7 |
    8 | @components.Carousel( 9 | components.CarouselProps{ 10 | {Source: "/static/images/avatar.jpg", Alt: "avatar image"}, 11 | {Source: "/static/images/avatar.jpg", Alt: "avatar image"}, 12 | {Source: "/static/images/avatar.jpg", Alt: "avatar image"}, 13 | {Source: "/static/images/avatar.jpg", Alt: "avatar image"}, 14 | {Source: "/static/images/avatar.jpg", Alt: "avatar image"}, 15 | {Source: "/static/images/avatar.jpg", Alt: "avatar image"}, 16 | }, 17 | ) 18 |
    19 | } 20 | -------------------------------------------------------------------------------- /internal/views/examples/chat.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "time" 5 | "github.com/haatos/goshipit/internal/views/components" 6 | ) 7 | 8 | // example 9 | templ BasicChat() { 10 |
    11 | @components.Chat(components.ChatProps{ 12 | { 13 | AvatarURL: "/static/images/avatar.jpg", 14 | Sender: "Me", 15 | Time: time.Now().UTC().Add(-4 * time.Minute).Format("15:04:05"), 16 | Message: `I started learning how to cook more dishes from scratch, 17 | and it's been way more satisfying than I expected. Last night, I made homemade pasta!`, 18 | Footer: "✓✓", 19 | Location: "start", 20 | Class: "chat-bubble-primary", 21 | }, 22 | { 23 | AvatarURL: "/static/images/avatar-reverse.jpg", 24 | Sender: "Myself", 25 | Time: time.Now().UTC().Add(-2 * time.Minute).Format("15:04:05"), 26 | Message: `That's awesome! Homemade pasta is no joke—it's a workout, 27 | too! Did you use a pasta machine, or go for the classic rolling pin method?`, 28 | Footer: "✓✓", 29 | Location: "end", 30 | }, 31 | { 32 | AvatarURL: "/static/images/avatar.jpg", 33 | Sender: "Me", 34 | Time: time.Now().UTC().Add(-1 * time.Minute).Format("15:04:05"), 35 | Message: `Went old-school with the rolling pin! Took forever, but it 36 | was worth it. I made a simple marinara sauce to go with it, and honestly, it was better 37 | than what I usually order.`, 38 | Footer: "✓✓", 39 | Location: "start", 40 | Class: "chat-bubble-primary", 41 | }, 42 | { 43 | AvatarURL: "/static/images/avatar-reverse.jpg", 44 | Sender: "Myself", 45 | Time: time.Now().UTC().Format("15:04:05"), 46 | Message: `Love it! Next up, you've got to try ravioli. It's a bit 47 | more work, but filling them with something like ricotta and spinach makes it feel 48 | super fancy. You'll impress everyone!`, 49 | Footer: "✓✓", 50 | Location: "end", 51 | }, 52 | }) 53 |
    54 | } 55 | -------------------------------------------------------------------------------- /internal/views/examples/checkbox.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | // Different size checkboxes 7 | templ DifferentSizeCheckboxes() { 8 |
    9 | @components.Checkbox( 10 | components.CheckboxProps{ 11 | Before: "Remember me", 12 | Name: "remember_me", 13 | Checked: false, 14 | Size: "xs", 15 | }, 16 | ) 17 | @components.Checkbox( 18 | components.CheckboxProps{ 19 | Before: "Remember me", 20 | Name: "remember_me", 21 | Checked: false, 22 | Size: "sm", 23 | }, 24 | ) 25 | @components.Checkbox( 26 | components.CheckboxProps{ 27 | Before: "Remember me", 28 | Name: "remember_me", 29 | Checked: false, 30 | }, 31 | ) 32 | @components.Checkbox( 33 | components.CheckboxProps{ 34 | Before: "Remember me", 35 | Name: "remember_me", 36 | Checked: false, 37 | Size: "lg", 38 | }, 39 | ) 40 | @components.Checkbox( 41 | components.CheckboxProps{ 42 | Before: "Remember me", 43 | Name: "remember_me", 44 | Checked: false, 45 | Size: "xl", 46 | }, 47 | ) 48 |
    49 | } 50 | 51 | // example 52 | // Checkbox with label after 53 | templ PrimaryCheckbox() { 54 |
    55 | @components.Checkbox( 56 | components.CheckboxProps{ 57 | After: "Remember me", 58 | Name: "remember_me", 59 | Checked: true, 60 | Class: "checkbox-primary", 61 | }, 62 | ) 63 |
    64 | } 65 | -------------------------------------------------------------------------------- /internal/views/examples/collapse.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | // Basic collapse 7 | templ CollapseExample() { 8 |
    9 | @components.Collapse( 10 | components.CollapseProps{ 11 | Title: "Click me to show/hide content", 12 | TitleClass: "text-xl font-medium", 13 | Class: "collapse-plus bg-base-300", 14 | ContentClass: "bg-base-200", 15 | }) { 16 |
    17 |

    Collapse content

    18 |
    19 | } 20 |
    21 | } 22 | -------------------------------------------------------------------------------- /internal/views/examples/combobox.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | templ BasicCombobox() { 7 |
    8 |
    9 | @components.Combobox(components.ComboboxProps{ 10 | Name: "example_combo", 11 | Label: "Example", 12 | URL: "/combobox/%s/%s", 13 | Options: []string{ 14 | "Thing 1", "Thing 2", "Thing 3", "Thing 4", "Thing 5", "Thing 6", "Thing 7", 15 | }, 16 | }) 17 | 18 | 30 |
    31 |
    32 | } 33 | -------------------------------------------------------------------------------- /internal/views/examples/countdown.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "github.com/haatos/goshipit/internal/views/components" 5 | "time" 6 | ) 7 | 8 | // example 9 | templ CountdownExample() { 10 |
    11 | @components.Countdown(time.Now().UTC().Add(time.Hour*48 - 40*time.Second)) 12 |
    13 | } 14 | -------------------------------------------------------------------------------- /internal/views/examples/date_picker.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "time" 4 | import "github.com/haatos/goshipit/internal/views/components" 5 | 6 | // example 7 | templ BasicDatePicker() { 8 | {{ now := time.Now().UTC() }} 9 | @components.DatePicker( 10 | components.DatePickerProps{ 11 | Year: now.Year(), 12 | Month: int(now.Month()), 13 | Selected: now, 14 | StartOfWeek: time.Monday, 15 | }, 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /internal/views/examples/diff.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | templ ImageDiff() { 7 |
    8 | @components.Diff( 9 | components.DiffProps{ 10 | Width: 16, 11 | Height: 19, 12 | Image1: components.DiffImage{ 13 | Source: "/static/images/diff1.png", Alt: "diff image 1", 14 | }, 15 | Image2: components.DiffImage{ 16 | Source: "/static/images/diff2.png", Alt: "diff image 2", 17 | }, 18 | }, 19 | ) 20 |
    21 | } 22 | -------------------------------------------------------------------------------- /internal/views/examples/drawer.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | templ BasicDrawer() { 7 |
    8 | @DrawerPreview(DrawerExampleToggle(), DrawerExampleMenu()) 9 |
    10 | } 11 | 12 | templ DrawerPreview(toggle templ.Component, sidebar templ.Component) { 13 |
    14 | 15 |
    16 | { children... } 17 | 20 |
    21 |
    22 | 23 | 26 |
    27 |
    28 | } 29 | 30 | templ DrawerExampleToggle() { 31 | Click me 32 | } 33 | 34 | templ DrawerExampleMenu() { 35 | @components.MenuItem(components.MenuItemProps{Label: "Section 1", Attrs: templ.Attributes{"class": "menu-title"}}) 36 | @components.MenuItem(components.MenuItemProps{Label: "Section 2", Attrs: templ.Attributes{"class": "menu-title"}}) { 37 | @components.MenuItem(components.MenuItemProps{Label: "2.1"}) { 38 | @components.MenuItem(components.MenuItemProps{Label: "2.1.1"}) 39 | @components.MenuItem(components.MenuItemProps{Label: "2.1.2"}) 40 | } 41 | @components.MenuItem(components.MenuItemProps{Label: "2.2"}) { 42 | @components.MenuItem(components.MenuItemProps{Label: "2.2.1"}) 43 | } 44 | @components.MenuItem(components.MenuItemProps{Label: "2.3"}) 45 | } 46 | @components.MenuItem(components.MenuItemProps{Label: "Section 3", Attrs: templ.Attributes{"class": "menu-title"}}) { 47 | @components.MenuItem(components.MenuItemProps{Label: "3.1"}) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/views/examples/dropdown.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | templ BasicDropdown() { 7 |
    8 | @components.Dropdown( 9 | components.DropdownProps{ 10 | Label: "Dropdown label", 11 | Items: []components.DropdownItem{ 12 | {Label: "Item 1"}, 13 | {Label: "Item 2"}, 14 | {Label: "Item 3"}, 15 | }, 16 | ListClass: "bg-base-200 rounded-box z-50 w-52 p-2 shadow-sm", 17 | }, 18 | ) 19 |
    20 | } 21 | -------------------------------------------------------------------------------- /internal/views/examples/file_input.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | // Different size file inputs 7 | templ DifferentSizeFileInputs() { 8 | @components.FileInput(components.FileInputProps{ 9 | Label: "File upload", 10 | Size: "xs", 11 | }) 12 | @components.FileInput(components.FileInputProps{ 13 | Label: "File upload", 14 | Size: "sm", 15 | }) 16 | @components.FileInput(components.FileInputProps{ 17 | Label: "File upload", 18 | }) 19 | @components.FileInput(components.FileInputProps{ 20 | Label: "File upload", 21 | Size: "lg", 22 | }) 23 | @components.FileInput(components.FileInputProps{ 24 | Label: "File upload", 25 | Size: "xl", 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /internal/views/examples/footer.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | templ BasicFooterWithLinks() { 7 | @components.Footer( 8 | components.FooterProps{ 9 | Icon: FooterCompanyInfoIconExample(), 10 | Name: "ACME Industries Ltd.", 11 | Description: "Providing reliable tech since 1992", 12 | Anchors: []components.AnchorProps{ 13 | {LeftIcon: XIcon()}, 14 | {LeftIcon: YoutubeIcon()}, 15 | {LeftIcon: FacebookIcon()}, 16 | }, 17 | }, 18 | ) { 19 | @components.FooterNav( 20 | "Services", 21 | []components.AnchorProps{ 22 | {Label: "Branding"}, 23 | {Label: "Design"}, 24 | {Label: "Marketing"}, 25 | {Label: "Advertisement"}, 26 | }, 27 | ) 28 | @components.FooterNav( 29 | "Company", 30 | []components.AnchorProps{ 31 | {Label: "About us"}, 32 | {Label: "Contact"}, 33 | {Label: "Jobs"}, 34 | {Label: "Press kit"}, 35 | }, 36 | ) 37 | @components.FooterNav( 38 | "Legal", 39 | []components.AnchorProps{ 40 | {Label: "Terms of use"}, 41 | {Label: "Privacy policy"}, 42 | {Label: "Cookie policy"}, 43 | }, 44 | ) 45 | } 46 | } 47 | 48 | templ FooterCompanyInfoIconExample() { 49 | 58 | 61 | 62 | } 63 | -------------------------------------------------------------------------------- /internal/views/examples/hero.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | templ BasicHero() { 7 |
    8 | @components.Hero(components.HeroProps{ 9 | Source: "/static/images/avatar.jpg", 10 | Alt: "hero avatar", 11 | Class: "bg-base-200 min-h-[600px]", 12 | }) { 13 |
    14 |

    Lorem ipsum!

    15 |

    16 | Lorem ipsum dolor sit, amet consectetur adipisicing elit. 17 | Ex quibusdam dicta necessitatibus! Deleniti temporibus iure 18 | porro cupiditate dolorum modi voluptate perferendis velit 19 | tempora repudiandae expedita, impedit omnis vitae. Laborum, 20 | dignissimos? 21 |

    22 | 23 |
    24 | } 25 | @components.Hero(components.HeroProps{ 26 | Source: "/static/images/avatar.jpg", 27 | Alt: "hero avatar", 28 | Class: "bg-base-200 min-h-[600px]", 29 | Reverse: true, 30 | }) { 31 |
    32 |

    Lorem ipsum!

    33 |

    34 | Lorem ipsum dolor sit, amet consectetur adipisicing elit. 35 | Ex quibusdam dicta necessitatibus! Deleniti temporibus iure 36 | porro cupiditate dolorum modi voluptate perferendis velit 37 | tempora repudiandae expedita, impedit omnis vitae. Laborum, 38 | dignissimos? 39 |

    40 | 41 |
    42 | } 43 |
    44 | } 45 | -------------------------------------------------------------------------------- /internal/views/examples/infinite_scroll.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "fmt" 5 | "github.com/haatos/goshipit/internal/views/components" 6 | ) 7 | 8 | // example 9 | templ InfiniteScrollTableExample() { 10 | @components.Table( 11 | []templ.Component{components.PlainText("Name"), components.PlainText("Email")}, 12 | initialRows(), 13 | nil, 14 | ) 15 |
    16 | } 17 | 18 | func initialRows() []templ.Component { 19 | data := make([]templ.Component, 10) 20 | for i := 0; i < 10; i++ { 21 | data[i] = components.InfiniteScrollRow("John Doe", fmt.Sprintf("john.doe%d@email.com", i), 0, i == 9) 22 | } 23 | 24 | return data 25 | } 26 | -------------------------------------------------------------------------------- /internal/views/examples/lazy_load.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | templ LazyLoadExample() { 7 | @components.LazyLoad("/lazy-load") 8 | } 9 | 10 | templ LazyLoadResult() { 11 |
    17 | LAZY 18 |
    19 | LOAD 20 |
    21 | COMPLETE 22 |
    23 | } 24 | -------------------------------------------------------------------------------- /internal/views/examples/modal.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "fmt" 5 | "github.com/haatos/goshipit/internal/views/components" 6 | ) 7 | 8 | // example 9 | // Basic modal 10 | templ BasicModal() { 11 |
    12 | @components.Modal(components.ModalProps{ID: "my_modal", Label: "Click me"}) { 13 |

    Modal title

    14 |

    Modal content goes here

    15 | } 16 |
    17 | } 18 | 19 | // example 20 | // Multiple modals 21 | templ MultipleModals() { 22 |
    23 |
    24 | for i := range 3 { 25 | @components.Modal(components.ModalProps{ID: fmt.Sprintf("my_modal_%d", i), Label: "Click me"}) { 26 |

    Modal title { fmt.Sprintf("%d", i) }

    27 |

    Modal content { fmt.Sprintf("%d", i) }

    28 | } 29 | } 30 |
    31 |
    32 | } 33 | 34 | // example 35 | // Modal with action button 36 | templ ModalWithAction() { 37 |
    38 | @components.Modal(components.ModalProps{ID: "my_modal_actions", Label: "Click me"}) { 39 |

    Modal title

    40 |

    Modal content goes here

    41 | 44 | } 45 |
    46 | } 47 | 48 | // example 49 | // Modal to confirm delete action 50 | templ ModalConfirmDelete() { 51 |
    52 |
    53 | for i := range 6 { 54 |
    55 | @components.Modal( 56 | components.ModalProps{ 57 | ID: fmt.Sprintf("modal_confirm_%d", i), 58 | Label: modalConfirmDeleteButton(i, templ.Attributes{"onclick": fmt.Sprintf("modal_confirm_%d.showModal()", i)}), 59 | }, 60 | ) { 61 |
    62 |
    63 |
    64 | 70 |
    71 |
    72 |

    73 | Are you sure you want to delete { fmt.Sprintf("%d", i) }? 74 |

    75 |
    76 |
    77 |
    78 | 87 | 90 |
    91 | 100 |
    101 | } 102 |
    103 | } 104 |
    105 |
    106 | } 107 | 108 | templ modalConfirmDeleteButton(i int, attrs templ.Attributes) { 109 | 112 | } 113 | -------------------------------------------------------------------------------- /internal/views/examples/pagination.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | templ BasicPaginationExample() { 7 | @BasicPagination( 8 | "example-pagination", 9 | components.PaginationProps{ 10 | URL: "/pagination-pages", 11 | Page: 1, 12 | Low: 1, 13 | High: 7, 14 | MaxPages: 20, 15 | }, 16 | [][]string{ 17 | {"Key0", "Value0"}, 18 | {"Key1", "Value1"}, 19 | {"Key2", "Value2"}, 20 | {"Key3", "Value3"}, 21 | {"Key4", "Value4"}, 22 | {"Key5", "Value5"}, 23 | {"Key6", "Value6"}, 24 | {"Key7", "Value7"}, 25 | {"Key8", "Value8"}, 26 | {"Key9", "Value9"}, 27 | }, 28 | ) 29 | } 30 | 31 | templ BasicPagination(id string, p components.PaginationProps, data [][]string) { 32 | @components.Pagination("example-pagination", p) { 33 |
    34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | for _, d := range data { 43 | 44 | 45 | 46 | 47 | } 48 | 49 |
    KeyValue
    { d[0] }{ d[1] }
    50 |
    51 | } 52 | } 53 | -------------------------------------------------------------------------------- /internal/views/examples/pricing.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | // Basic pricing section 7 | templ PricingExample() { 8 | @components.Pricing( 9 | components.PricingProps{ 10 | Checked: true, 11 | Prices: []components.PriceProps{ 12 | { 13 | Title: "Free", 14 | Description: "My free plan", 15 | PriceMonthly: "$ 0", 16 | PerMonthly: "/ month", 17 | PriceAnnually: "$ 0", 18 | PerAnnually: "/ month", 19 | IncludedFeatures: []string{"Feature 1", "Feature 2", "Feature 3"}, 20 | ExcludedFeatures: []string{"Feature 4", "Feature 5"}, 21 | CallToAction: components.PriceButtonProps{ 22 | Label: "Start free", 23 | Attrs: templ.Attributes{ 24 | "class": "btn btn-outline mt-8", 25 | }, 26 | }, 27 | }, 28 | { 29 | Title: "Starter", 30 | Description: "Starter plan", 31 | PriceMonthly: "$ 12", 32 | PerMonthly: "/ month", 33 | PriceAnnually: "$ 10", 34 | PerAnnually: "/ month", 35 | IncludedFeatures: []string{"Feature 1", "Feature 2", "Feature 3"}, 36 | ExcludedFeatures: []string{"Feature 4", "Feature 5"}, 37 | CallToAction: components.PriceButtonProps{ 38 | Label: "Get started", 39 | Attrs: templ.Attributes{ 40 | "class": "btn btn-primary mt-8", 41 | }, 42 | }, 43 | }, 44 | { 45 | Title: "Professional", 46 | Description: "Professional plan", 47 | PriceMonthly: "$ 20", 48 | PerMonthly: "/ month", 49 | PriceAnnually: "$ 16", 50 | PerAnnually: "/ month", 51 | IncludedFeatures: []string{"Feature 1", "Feature 2", "Feature 3"}, 52 | ExcludedFeatures: []string{"Feature 4", "Feature 5"}, 53 | CallToAction: components.PriceButtonProps{ 54 | Label: "Get started", 55 | Attrs: templ.Attributes{ 56 | "class": "btn btn-primary mt-8", 57 | }, 58 | }, 59 | }, 60 | }, 61 | }, 62 | ) 63 | } 64 | 65 | // example 66 | // Pricing section with promotion 67 | templ PricingWithPromotionExample() { 68 | @components.Pricing( 69 | components.PricingProps{ 70 | Checked: true, 71 | Prices: []components.PriceProps{ 72 | { 73 | Title: "Free", 74 | Description: "My free plan", 75 | PriceMonthly: "$ 0", 76 | PerMonthly: "/ month", 77 | PriceAnnually: "$ 0", 78 | PerAnnually: "/ month", 79 | IncludedFeatures: []string{"Feature 1", "Feature 2", "Feature 3"}, 80 | ExcludedFeatures: []string{"Feature 4", "Feature 5"}, 81 | CallToAction: components.PriceButtonProps{ 82 | Label: "Start free", 83 | Attrs: templ.Attributes{ 84 | "class": "btn btn-outline mt-8", 85 | }, 86 | }, 87 | }, 88 | { 89 | Title: "Starter", 90 | Description: "Starter plan", 91 | PriceMonthly: "$ 12", 92 | PerMonthly: "/ month", 93 | PriceAnnually: "$ 10", 94 | PerAnnually: "/ month", 95 | IncludedFeatures: []string{"Feature 1", "Feature 2", "Feature 3"}, 96 | ExcludedFeatures: []string{"Feature 4", "Feature 5"}, 97 | CallToAction: components.PriceButtonProps{ 98 | Label: "Get started", 99 | Attrs: templ.Attributes{ 100 | "class": "btn btn-primary mt-8", 101 | }, 102 | }, 103 | }, 104 | { 105 | Title: "Professional", 106 | Description: "Professional plan", 107 | PriceMonthly: "$ 20", 108 | PerMonthly: "/ month", 109 | PriceAnnually: "$ 16", 110 | PerAnnually: "/ month", 111 | Promotion: "Popular", 112 | IncludedFeatures: []string{"Feature 1", "Feature 2", "Feature 3"}, 113 | ExcludedFeatures: []string{"Feature 4", "Feature 5"}, 114 | CallToAction: components.PriceButtonProps{ 115 | Label: "Get started", 116 | Attrs: templ.Attributes{ 117 | "class": "btn btn-primary mt-8", 118 | }, 119 | }, 120 | }, 121 | { 122 | Title: "Ultimate", 123 | Description: "Ultimate plan", 124 | PriceMonthly: "$ 30", 125 | PerMonthly: "/ month", 126 | PriceAnnually: "$ 25", 127 | PerAnnually: "/ month", 128 | IncludedFeatures: []string{"Feature 1", "Feature 2", "Feature 3", "Feature 4", "Feature 5"}, 129 | CallToAction: components.PriceButtonProps{ 130 | Label: "Get started", 131 | Attrs: templ.Attributes{ 132 | "class": "btn btn-primary mt-8", 133 | }, 134 | }, 135 | }, 136 | }, 137 | }, 138 | ) 139 | } 140 | -------------------------------------------------------------------------------- /internal/views/examples/radio.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | // Different size radio groups 7 | templ DefaultRadio() { 8 |
    9 | @components.Radio( 10 | components.RadioProps{ 11 | Name: "my-radio-group1", 12 | Values: map[string]string{ 13 | "Apples": "apples", 14 | "Oranges": "oranges", 15 | }, 16 | Size: "xs", 17 | }, 18 | ) 19 |
    20 | @components.Radio( 21 | components.RadioProps{ 22 | Name: "my-radio-group1", 23 | Values: map[string]string{ 24 | "Apples": "apples", 25 | "Oranges": "oranges", 26 | }, 27 | Size: "sm", 28 | }, 29 | ) 30 |
    31 | @components.Radio( 32 | components.RadioProps{ 33 | Name: "my-radio-group1", 34 | Values: map[string]string{ 35 | "Apples": "apples", 36 | "Oranges": "oranges", 37 | }, 38 | }, 39 | ) 40 |
    41 | @components.Radio( 42 | components.RadioProps{ 43 | Name: "my-radio-group1", 44 | Values: map[string]string{ 45 | "Apples": "apples", 46 | "Oranges": "oranges", 47 | }, 48 | Size: "lg", 49 | }, 50 | ) 51 |
    52 | @components.Radio( 53 | components.RadioProps{ 54 | Name: "my-radio-group1", 55 | Values: map[string]string{ 56 | "Apples": "apples", 57 | "Oranges": "oranges", 58 | }, 59 | Size: "xl", 60 | }, 61 | ) 62 |
    63 | } 64 | 65 | // example 66 | // Primary radio group 67 | templ PrimaryRadio() { 68 |
    69 | @components.Radio( 70 | components.RadioProps{ 71 | Name: "my-radio-group2", 72 | Values: map[string]string{ 73 | "Apples": "apples", 74 | "Oranges": "oranges", 75 | "And something truly special here to see how this works with a longer key": "blaaaaa", 76 | }, 77 | Class: "radio-primary", 78 | }, 79 | ) 80 |
    81 | } 82 | -------------------------------------------------------------------------------- /internal/views/examples/range.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | // Range component using alpine.js 7 | templ BasicRange() { 8 |
    9 | @components.Range( 10 | components.RangeProps{ 11 | Name: "my-range", 12 | Value: 25, 13 | Min: 0, 14 | Max: 100, 15 | Step: 5, 16 | Size: "xs", 17 | }, 18 | ) 19 | @components.Range( 20 | components.RangeProps{ 21 | Name: "my-range", 22 | Value: 25, 23 | Min: 0, 24 | Max: 100, 25 | Step: 5, 26 | Size: "sm", 27 | }, 28 | ) 29 | @components.Range( 30 | components.RangeProps{ 31 | Name: "my-range", 32 | Value: 25, 33 | Min: 0, 34 | Max: 100, 35 | Step: 5, 36 | }, 37 | ) 38 | @components.Range( 39 | components.RangeProps{ 40 | Name: "my-range", 41 | Value: 25, 42 | Min: 0, 43 | Max: 100, 44 | Step: 5, 45 | Size: "lg", 46 | }, 47 | ) 48 | @components.Range( 49 | components.RangeProps{ 50 | Name: "my-range", 51 | Value: 25, 52 | Min: 0, 53 | Max: 100, 54 | Step: 5, 55 | Size: "xl", 56 | }, 57 | ) 58 |
    59 | } 60 | 61 | // example 62 | // Range component using datastar.js 63 | templ DatastarRange() { 64 |
    65 | @components.DatastarRange( 66 | components.RangeProps{ 67 | Name: "my-range", 68 | Value: 25, 69 | Min: 0, 70 | Max: 100, 71 | Step: 5, 72 | }, 73 | ) 74 |
    75 | } 76 | -------------------------------------------------------------------------------- /internal/views/examples/rating.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | // Rating from 1 to 5 7 | templ RatingFromOneToFive() { 8 |
    9 | @components.Rating( 10 | components.RatingProps{ 11 | Name: "my-rating", 12 | Min: 1, 13 | Max: 5, 14 | }, 15 | ) 16 |
    17 | } 18 | 19 | // example 20 | // Rating from 0 to 5 21 | templ RatingFromZeroToFive() { 22 |
    23 | @components.Rating( 24 | components.RatingProps{ 25 | Name: "my-rating2", 26 | Min: 0, 27 | Max: 5, 28 | }, 29 | ) 30 |
    31 | } 32 | -------------------------------------------------------------------------------- /internal/views/examples/select.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | // Different size selects 7 | templ DifferentSizeSelects() { 8 |
    9 | @components.Select( 10 | components.SelectProps{ 11 | Label: "Make a choice", 12 | Name: "my-select", 13 | Options: []components.SelectOption{ 14 | {Label: "Which one?", Selected: true, Disabled: true}, 15 | {Label: "Apples", Value: "apples"}, 16 | {Label: "Oranges", Value: "oranges"}, 17 | }, 18 | Size: "xs", 19 | }, 20 | ) 21 | @components.Select( 22 | components.SelectProps{ 23 | Label: "Make a choice", 24 | Name: "my-select", 25 | Options: []components.SelectOption{ 26 | {Label: "Which one?", Selected: true, Disabled: true}, 27 | {Label: "Apples", Value: "apples"}, 28 | {Label: "Oranges", Value: "oranges"}, 29 | }, 30 | Size: "sm", 31 | }, 32 | ) 33 | @components.Select( 34 | components.SelectProps{ 35 | Label: "Make a choice", 36 | Name: "my-select", 37 | Options: []components.SelectOption{ 38 | {Label: "Which one?", Selected: true, Disabled: true}, 39 | {Label: "Apples", Value: "apples"}, 40 | {Label: "Oranges", Value: "oranges"}, 41 | }, 42 | }, 43 | ) 44 | @components.Select( 45 | components.SelectProps{ 46 | Label: "Make a choice", 47 | Name: "my-select", 48 | Options: []components.SelectOption{ 49 | {Label: "Which one?", Selected: true, Disabled: true}, 50 | {Label: "Apples", Value: "apples"}, 51 | {Label: "Oranges", Value: "oranges"}, 52 | }, 53 | Size: "lg", 54 | }, 55 | ) 56 | @components.Select( 57 | components.SelectProps{ 58 | Label: "Make a choice", 59 | Name: "my-select", 60 | Options: []components.SelectOption{ 61 | {Label: "Which one?", Selected: true, Disabled: true}, 62 | {Label: "Apples", Value: "apples"}, 63 | {Label: "Oranges", Value: "oranges"}, 64 | }, 65 | Size: "xl", 66 | }, 67 | ) 68 |
    69 | } 70 | 71 | // example 72 | // Cascading select 73 | templ CascadingSelect() { 74 |
    75 | @components.Select( 76 | components.SelectProps{ 77 | Label: "Make", 78 | Name: "make", 79 | Options: []components.SelectOption{ 80 | {Label: "Audi", Value: "audi"}, 81 | {Label: "BMW", Value: "bmw"}, 82 | {Label: "Toyota", Value: "toyota"}, 83 | }, 84 | Attrs: templ.Attributes{ 85 | "hx-get": "/models", 86 | "hx-target": "#models", 87 | }, 88 | }, 89 | ) 90 | @components.Select( 91 | components.SelectProps{ 92 | Label: "Model", 93 | Name: "model", 94 | Options: []components.SelectOption{ 95 | {Label: "A1", Value: "a1"}, 96 | {Label: "A4", Value: "a4"}, 97 | {Label: "A6", Value: "a6"}, 98 | }, 99 | Attrs: templ.Attributes{ 100 | "id": "models", 101 | }, 102 | }, 103 | ) 104 |
    105 | } 106 | -------------------------------------------------------------------------------- /internal/views/examples/skeleton.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | templ SkeletonExample() { 7 |
    8 | @components.Skeleton() 9 |
    10 | } -------------------------------------------------------------------------------- /internal/views/examples/stats.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | templ BasicStats() { 7 |
    8 | @components.Stats() { 9 | @components.Stat(components.StatProps{ 10 | Title: "Downloads", 11 | Value: "31k", 12 | Description: "Jan 1st - Feb 1st", 13 | }) 14 | @components.Stat(components.StatProps{ 15 | Title: "New Users", 16 | Value: "4,200", 17 | Description: "↗︎ 400 (22%)", 18 | }) 19 | @components.Stat(components.StatProps{ 20 | Title: "New Registers", 21 | Value: "1,200", 22 | Description: "↘︎ 90 (14%)", 23 | }) 24 | } 25 |
    26 | } 27 | -------------------------------------------------------------------------------- /internal/views/examples/status.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | // 404 Not Found status 7 | templ StatusNotFound() { 8 | @components.Status(components.StatusProps{ 9 | Code: 404, 10 | Title: "Not Found", 11 | Description: "Looks like there's nothing here...", 12 | ReturnButtonLabel: "Go back", 13 | }) 14 | } 15 | 16 | // example 17 | // 403 Forbidden status 18 | templ StatusForbidden() { 19 | @components.Status(components.StatusProps{ 20 | Code: 403, 21 | Title: "Forbidden", 22 | Description: "Invalid permissions to view this page.", 23 | ReturnButtonLabel: "Go back", 24 | }) 25 | } 26 | 27 | // example 28 | // 401 Unauthorized status 29 | templ StatusUnauthorized() { 30 | @components.Status(components.StatusProps{ 31 | Code: 401, 32 | Title: "Unauthorized", 33 | Description: "This page is only available to authenticated users.", 34 | ReturnButtonLabel: "Go back", 35 | }) 36 | } 37 | 38 | // example 39 | // 500 Internal Server Error status 40 | templ StatusInternalServerError() { 41 | @components.Status(components.StatusProps{ 42 | Code: 500, 43 | Title: "Internal Server Error", 44 | Description: "Something went terribly wrong...", 45 | ReturnButtonLabel: "Go back", 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /internal/views/examples/steps.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | templ BasicSteps() { 7 |
    8 | @components.Steps() { 9 | @components.Step(components.StepProps{Label: "Register", Done: true}) 10 | @components.Step(components.StepProps{Label: "Choose plan", Done: true}) 11 | @components.Step(components.StepProps{Label: "Purchase"}) 12 | @components.Step(components.StepProps{Label: "Receive product"}) 13 | } 14 |
    15 | } 16 | -------------------------------------------------------------------------------- /internal/views/examples/swap.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | templ BasicSwap() { 7 |
    8 | @components.Swap( 9 | components.SwapProps{ 10 | On: SwapExampleOn(), 11 | Off: SwapExampleOff(), 12 | Class: "swap-flip", 13 | }, 14 | ) 15 |
    16 | } 17 | 18 | templ SwapExampleOn() { 19 | ON 20 | } 21 | 22 | templ SwapExampleOff() { 23 | OFF 24 | } 25 | -------------------------------------------------------------------------------- /internal/views/examples/table.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | templ BasicTable() { 7 | @components.Table( 8 | []templ.Component{ 9 | components.Checkbox( 10 | components.CheckboxProps{ 11 | Name: "all", 12 | }, 13 | ), 14 | components.PlainText("Name"), 15 | components.PlainText("Email"), 16 | }, 17 | []templ.Component{ 18 | TableExampleRow("John Doe", "john.doe@example.com"), 19 | TableExampleRow("Jane Doe", "Jane.doe@example.com"), 20 | TableExampleRow("Jim Smith", "jim.smith@example.com"), 21 | TableExampleRow("Julie Smith", "julie.smith@example.com"), 22 | }, 23 | nil, 24 | ) 25 | } 26 | 27 | templ TableExampleRow(name, email string) { 28 | 29 | 30 | @components.Checkbox(components.CheckboxProps{Name: email}) 31 | 32 | 33 | @components.PlainText(name) 34 | 35 | 36 | @components.PlainText(email) 37 | 38 | 39 | } 40 | -------------------------------------------------------------------------------- /internal/views/examples/tabs.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | templ BasicTabs() { 7 | @components.Tabs( 8 | components.TabsProps{ 9 | Name: "basic-tabs", 10 | Class: "tabs-border", 11 | ContentClass: "bg-base-100 py-8", 12 | Tabs: []components.TabProps{ 13 | { 14 | Label: "Home", 15 | Content: homeTabContent(), 16 | }, 17 | { 18 | Label: "Info", 19 | Content: infoTabContent(), 20 | }, 21 | { 22 | Label: "Stats", 23 | Content: statsTabContent(), 24 | }, 25 | }, 26 | }) 27 | } 28 | 29 | templ homeTabContent() { 30 |

    This is the home tab

    31 | } 32 | 33 | templ infoTabContent() { 34 |

    This is the info tab

    35 | } 36 | 37 | templ statsTabContent() { 38 |

    This is the stats tab

    39 | } 40 | -------------------------------------------------------------------------------- /internal/views/examples/testimonial.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | templ TestimonialGridExample() { 7 | @components.TestimonialGrid( 8 | "Read what our customers think", 9 | components.TestimonialProps{ 10 | { 11 | Avatar: components.Avatar(components.AvatarProps{ 12 | ContainerClass: "rounded h-20", 13 | Source: "/static/images/avatar.jpg", 14 | }), 15 | Name: "Jane Doe", 16 | Rating: 5, 17 | Content: `Lorem ipsum dolor sit amet consectetur adipisicing elit. 18 | Iure impedit, placeat sed provident enim fuga possimus ducimus est iusto inventore 19 | earum aliquid officia minus, maiores quia dicta magni ex labore? Lorem ipsum dolor 20 | sit amet consectetur adipisicing elit. Libero maxime quos laboriosam natus illum 21 | similique id nam, rerum, sunt veritatis dolorum accusamus voluptas odio minus 22 | necessitatibus perspiciatis, aliquid repellat iste.`, 23 | }, 24 | { 25 | Avatar: components.Avatar( 26 | components.AvatarProps{ 27 | ContainerClass: "rounded h-20", 28 | Source: "/static/images/avatar.jpg"}, 29 | ), 30 | Name: "Jane Doe", 31 | Rating: 4, 32 | Content: `maiores quia dicta magni ex labore? Lorem ipsum dolor 33 | sit amet consectetur adipisicing elit. Libero maxime quos laboriosam natus illum 34 | similique id nam, rerum, sunt veritatis dolorum accusamus voluptas odio minus 35 | necessitatibus perspiciatis, aliquid repellat iste.`, 36 | }, 37 | { 38 | Avatar: components.Avatar(components.AvatarProps{ 39 | ContainerClass: "rounded h-20", 40 | Source: "/static/images/avatar.jpg", 41 | }), 42 | Name: "Jane Doe", 43 | Rating: 3, 44 | Content: `Iure impedit, placeat sed provident enim fuga possimus 45 | ducimus est iusto inventore earum aliquid officia minus, maiores quia dicta magni 46 | ex labore? Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero maxime 47 | quos laboriosam natus illum similique id nam, rerum, sunt veritatis dolorum 48 | accusamus voluptas odio minus necessitatibus perspiciatis, aliquid repellat iste.`, 49 | }, 50 | { 51 | Avatar: components.Avatar(components.AvatarProps{ 52 | ContainerClass: "rounded h-20", 53 | Source: "/static/images/avatar.jpg", 54 | }), 55 | Name: "Jane Doe", 56 | Rating: 5, 57 | Content: `Lorem ipsum dolor sit amet consectetur adipisicing elit. 58 | Iure impedit, placeat sed provident enim fuga possimus ducimus est iusto inventore 59 | earum aliquid officia minus, maiores quia dicta magni ex labore? Lorem ipsum dolor 60 | sit amet consectetur adipisicing elit. Libero maxime quos laboriosam natus illum 61 | similique id nam, rerum, sunt veritatis dolorum accusamus voluptas odio minus 62 | necessitatibus perspiciatis, aliquid repellat iste.`, 63 | }, 64 | { 65 | Avatar: components.Avatar(components.AvatarProps{ 66 | ContainerClass: "rounded h-20", 67 | Source: "/static/images/avatar.jpg", 68 | }), 69 | Name: "Jane Doe", 70 | Rating: 5, 71 | Content: "Lorem ipsum dolor sit amet consectetur adipisicing elit.", 72 | }, 73 | { 74 | Avatar: components.Avatar(components.AvatarProps{ 75 | ContainerClass: "rounded h-20", 76 | Source: "/static/images/avatar.jpg", 77 | }), 78 | Name: "Jane Doe", 79 | Rating: 1, 80 | Content: `Lorem ipsum dolor sit amet consectetur adipisicing elit. 81 | Iure impedit, placeat sed provident enim fuga possimus ducimus est iusto inventore 82 | earum aliquid officia minus.`, 83 | }, 84 | { 85 | Avatar: components.Avatar(components.AvatarProps{ 86 | ContainerClass: "rounded h-20", 87 | Source: "/static/images/avatar.jpg", 88 | }), 89 | Name: "Jane Doe", 90 | Rating: 2, 91 | Content: `Lorem ipsum dolor sit amet consectetur adipisicing elit. 92 | Iure impedit, placeat sed provident enim fuga possimus ducimus est iusto inventore 93 | earum aliquid officia minus, maiores quia dicta magni ex labore? Lorem ipsum dolor 94 | sit amet consectetur adipisicing elit.`, 95 | }, 96 | { 97 | Avatar: components.Avatar(components.AvatarProps{ 98 | ContainerClass: "rounded h-20", 99 | Source: "/static/images/avatar.jpg", 100 | }), 101 | Name: "Jane Doe", 102 | Rating: 5, 103 | Content: `Lorem ipsum dolor sit amet consectetur adipisicing elit. 104 | Iure impedit, placeat sed provident enim fuga possimus ducimus est iusto inventore 105 | earum aliquid officia minus, maiores quia dicta magni ex labore? Lorem ipsum dolor 106 | sit amet consectetur adipisicing elit. Libero maxime quos laboriosam natus illum 107 | similique id nam, rerum, sunt veritatis.`, 108 | }, 109 | { 110 | Avatar: components.Avatar(components.AvatarProps{ 111 | ContainerClass: "rounded h-20", 112 | Source: "/static/images/avatar.jpg", 113 | }), 114 | Name: "Jane Doe", 115 | Rating: 4, 116 | Content: `Lorem ipsum dolor sit amet consectetur adipisicing elit. 117 | Iure impedit, placeat sed provident enim fuga possimus ducimus est iusto inventore 118 | earum aliquid officia minus, maiores quia dicta magni ex labore? Lorem ipsum dolor 119 | sit amet consectetur adipisicing elit. Libero maxime quos laboriosam natus illum 120 | similique id nam, rerum.`, 121 | }, 122 | { 123 | Avatar: components.Avatar(components.AvatarProps{ 124 | ContainerClass: "rounded h-20", 125 | Source: "/static/images/avatar.jpg", 126 | }), 127 | Name: "Jane Doe", 128 | Rating: 4, 129 | Content: `Lorem ipsum dolor sit amet consectetur adipisicing elit. 130 | Iure impedit, placeat sed provident enim fuga possimus ducimus est iusto inventore 131 | earum aliquid officia minus, maiores quia dicta magni ex labore? Lorem ipsum dolor 132 | sit amet consectetur adipisicing elit. Libero maxime quos laboriosam natus illum 133 | similique id nam, rerum, sunt veritatis dolorum accusamus voluptas.`, 134 | }, 135 | }, 136 | ) 137 | } 138 | -------------------------------------------------------------------------------- /internal/views/examples/textarea.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | // Basic textarea 7 | templ BasicTextarea() { 8 |
    9 | @components.Textarea( 10 | components.TextareaProps{ 11 | Label: "Description", 12 | Name: "description", 13 | Class: "textarea-bordered resize-none", 14 | }, 15 | ) 16 |
    17 | } 18 | 19 | // example 20 | // Different sizes 21 | templ DifferentSizeTextareas() { 22 |
    23 | @components.Textarea( 24 | components.TextareaProps{ 25 | Label: "Description", 26 | Class: "textarea-bordered resize-none", 27 | Value: "Extra small", 28 | Size: "xs", 29 | }, 30 | ) 31 | @components.Textarea( 32 | components.TextareaProps{ 33 | Label: "Description", 34 | Class: "textarea-bordered resize-none", 35 | Value: "Small", 36 | Size: "sm", 37 | }, 38 | ) 39 | @components.Textarea( 40 | components.TextareaProps{ 41 | Label: "Description", 42 | Class: "textarea-bordered resize-none", 43 | Value: "Medium", 44 | }, 45 | ) 46 | @components.Textarea( 47 | components.TextareaProps{ 48 | Label: "Description", 49 | Class: "textarea-bordered resize-none", 50 | Value: "Large", 51 | Size: "lg", 52 | }, 53 | ) 54 | @components.Textarea( 55 | components.TextareaProps{ 56 | Label: "Description", 57 | Class: "textarea-bordered resize-none", 58 | Value: "Extra Large", 59 | Size: "xl", 60 | }, 61 | ) 62 |
    63 | } 64 | 65 | // example 66 | // Textarea with error 67 | templ BasicTextareaWithError() { 68 |
    69 | @components.Textarea( 70 | components.TextareaProps{ 71 | Label: "Description", 72 | Name: "description", 73 | Err: "Description cannot be empty", 74 | Class: "textarea-bordered resize-none", 75 | }, 76 | ) 77 |
    78 | } 79 | -------------------------------------------------------------------------------- /internal/views/examples/time_slot_picker.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "time" 4 | 5 | // example 6 | // Basic time slot picker 7 | templ BasicTimeSlotPicker() { 8 |
    9 | } 10 | -------------------------------------------------------------------------------- /internal/views/examples/timeline.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | templ BasicTimeline() { 7 |
    8 | @components.Timeline( 9 | components.TimelineProps{ 10 | {Start: "1984", Middle: components.TimelineCheckbox(true), End: "First Macintosh computer"}, 11 | {Start: "1998", Middle: components.TimelineCheckbox(true), End: "iMac"}, 12 | {Start: "2001", Middle: components.TimelineCheckbox(false), End: "iPod"}, 13 | {Start: "2007", Middle: components.TimelineCheckbox(false), End: "iPhone"}, 14 | {Start: "2015", Middle: components.TimelineCheckbox(false), End: "Apple Watch"}, 15 | }, 16 | ) 17 |
    18 | } 19 | -------------------------------------------------------------------------------- /internal/views/examples/toast.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | // Info-type toast 7 | templ InfoToast() { 8 |
    9 | @components.Toast( 10 | components.ToastProps{ 11 | Name: "info-toast", 12 | ToastClass: "absolute toast-end toast-top", 13 | AlertClass: "alert-info", 14 | }, 15 | ) { 16 | Info toast 17 | } 18 |
    19 | } 20 | 21 | // example 22 | // Warning-type toast 23 | templ WarningToast() { 24 |
    25 | @components.Toast( 26 | components.ToastProps{ 27 | Name: "warning-toast", 28 | ToastClass: "absolute toast-end toast-bottom", 29 | AlertClass: "alert-warning", 30 | }, 31 | ) { 32 | Warning toast 33 | } 34 |
    35 | } 36 | 37 | // example 38 | // Error-type toast 39 | templ ErrorToast() { 40 |
    41 | @components.Toast( 42 | components.ToastProps{ 43 | Name: "error-toast", 44 | ToastClass: "absolute toast-center toast-top", 45 | AlertClass: "alert-error", 46 | }, 47 | ) { 48 | Error toast 49 | } 50 |
    51 | } 52 | 53 | // example 54 | // Info-type toast with button to remove it 55 | templ InfoToastConfirm() { 56 |
    57 | @components.Toast(components.ToastProps{ 58 | Name: "error-toast", 59 | ToastClass: "absolute toast-end toast-top", 60 | AlertClass: "alert-info", 61 | }, 62 | ) { 63 | Info toast 64 | 65 | 72 | } 73 |
    74 | } 75 | -------------------------------------------------------------------------------- /internal/views/examples/toggle.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | // Different size toggles 7 | templ DifferentSizeToggles() { 8 |
    9 | @components.Toggle( 10 | components.ToggleProps{ 11 | Before: "Check me out", 12 | Name: "checkbox1", 13 | Size: "xs", 14 | }, 15 | ) 16 | @components.Toggle( 17 | components.ToggleProps{ 18 | Before: "Check me out", 19 | Name: "checkbox1", 20 | Size: "sm", 21 | }, 22 | ) 23 | @components.Toggle( 24 | components.ToggleProps{ 25 | Before: "Check me out", 26 | Name: "checkbox1", 27 | }, 28 | ) 29 | @components.Toggle( 30 | components.ToggleProps{ 31 | Before: "Check me out", 32 | Name: "checkbox1", 33 | Size: "lg", 34 | }, 35 | ) 36 | @components.Toggle( 37 | components.ToggleProps{ 38 | Before: "Check me out", 39 | Name: "checkbox1", 40 | Size: "xl", 41 | }, 42 | ) 43 |
    44 | } 45 | 46 | // example 47 | // Primary toggle with label after 48 | templ PrimaryToggle() { 49 |
    50 | @components.Toggle( 51 | components.ToggleProps{ 52 | After: "Check me out", 53 | Name: "checkbox2", 54 | Checked: true, 55 | Class: "toggle-primary", 56 | }, 57 | ) 58 |
    59 | } 60 | 61 | // example 62 | // Primary toggle with highlight 63 | templ PrimaryToggleWithHighlight() { 64 |
    65 | @components.Toggle( 66 | components.ToggleProps{ 67 | Before: "Paid monthly", 68 | After: "Paid annually", 69 | Name: "checkbox3", 70 | Highlight: true, 71 | Class: "toggle-primary", 72 | }, 73 | ) 74 |
    75 | } 76 | -------------------------------------------------------------------------------- /internal/views/examples/tooltip.templ: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | // example 6 | // Basic tooltip at the top 7 | templ BasicTooltip() { 8 |
    9 | @components.Tooltip(components.TooltipProps{Tip: "Hello"}) { 10 | 11 | } 12 |
    13 | } 14 | 15 | // example 16 | // Error-type tooltip on the bottom 17 | templ BasicTooltipError() { 18 |
    19 | @components.Tooltip( 20 | components.TooltipProps{ 21 | Tip: "Hello", 22 | Class: "tooltip-bottom tooltip-error", 23 | }, 24 | ) { 25 | 26 | } 27 |
    28 | } 29 | -------------------------------------------------------------------------------- /internal/views/pages/client_error.templ: -------------------------------------------------------------------------------- 1 | package pages 2 | 3 | import "github.com/haatos/goshipit/internal/views/components" 4 | 5 | templ NotFound() { 6 | @SideNavLayout(nil) { 7 | @components.Status(components.StatusProps{ 8 | Code: 404, 9 | Title: "Not Found", 10 | Description: "There seems to be nothing here.", 11 | ReturnButtonLabel: "Go back home", 12 | ReturnButtonAttrs: templ.Attributes{ 13 | "hx-get": "/", 14 | "hx-target": "main", 15 | "hx-swap": "innerHTML", 16 | "hx-push-url": "true", 17 | }, 18 | }) 19 | } 20 | } 21 | 22 | templ Forbidden(message string) { 23 | @SideNavLayout(nil) { 24 | @components.Status(components.StatusProps{ 25 | Code: 403, 26 | Title: "Forbidden", 27 | Description: "Invalid permissions to acces this page.", 28 | ReturnButtonLabel: "Go back home", 29 | ReturnButtonAttrs: templ.Attributes{ 30 | "hx-get": "/", 31 | "hx-target": "main", 32 | "hx-swap": "innerHTML", 33 | "hx-push-url": "true", 34 | }, 35 | }) 36 | } 37 | } 38 | 39 | templ Unauthorized() { 40 | @SideNavLayout(nil) { 41 | @components.Status( 42 | components.StatusProps{ 43 | Code: 401, 44 | Title: "Unauthorized", 45 | Description: "This page is for authenticated users only.", 46 | ReturnButtonLabel: "Go back home", 47 | ReturnButtonAttrs: templ.Attributes{ 48 | "hx-get": "/", 49 | "hx-target": "main", 50 | "hx-swap": "innerHTML", 51 | "hx-push-url": "true", 52 | }, 53 | }, 54 | ) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/views/pages/component.templ: -------------------------------------------------------------------------------- 1 | package pages 2 | 3 | import ( 4 | "github.com/haatos/goshipit/internal/markdown" 5 | "github.com/haatos/goshipit/internal/model" 6 | "github.com/haatos/goshipit/internal/views/components" 7 | "github.com/haatos/goshipit/internal/views/scripts" 8 | ) 9 | 10 | templ ComponentPage(cc model.ComponentCode, examples []templ.Component) { 11 | @SideNavLayout(nil) { 12 | @ComponentMain(cc, examples) 13 | } 14 | @scripts.CodeCopyButtonScript() 15 | } 16 | 17 | templ ComponentMain(cc model.ComponentCode, examples []templ.Component) { 18 |
    19 |
    20 |

    21 | { cc.Label } 22 | if cc.DaisyUIURL != "" { 23 | @components.Anchor(components.AnchorProps{ 24 | Label: "@DaisyUI", 25 | Href: cc.DaisyUIURL, 26 | Class: "text-sm link link-primary", 27 | Attrs: templ.Attributes{"target": "_blank"}, 28 | RightIcon: externalLinkIcon(), 29 | }) 30 | } 31 |

    32 |
    33 | @templ.Raw(markdown.GetHTMLFromMarkdown([]byte(cc.Description))) 34 |
    35 | if cc.Code != "" { 36 |

    Code

    37 |
    38 | @templ.Raw(markdown.GetHTMLFromMarkdown([]byte(cc.Code))) 39 |
    40 | } 41 | if len(examples) > 0 { 42 |

    Examples

    43 | } 44 |
    45 | for _, example := range examples { 46 | @example 47 | } 48 |
    49 |
    50 |
    51 | @scripts.HXCodeCopyButtonScript() 52 | } 53 | 54 | templ externalLinkIcon() { 55 | 56 | 57 | 64 | 65 | 66 | } 67 | 68 | templ ComponentExampleCode(code string) { 69 |
    70 | @templ.Raw(markdown.GetHTMLFromMarkdown([]byte(code))) 71 |
    72 | @scripts.HXCodeCopyButtonScript() 73 | } 74 | 75 | templ RawHTML(html string) { 76 | @templ.Raw(html) 77 | } 78 | 79 | templ ComponentExampleTabs(title, description string, tabs templ.Component) { 80 | if title != "" { 81 |

    { title }

    82 | } 83 | @templ.Raw(markdown.GetHTMLFromMarkdown([]byte(description))) 84 | @tabs 85 | } 86 | 87 | templ ComponentTabs(name string, tabs []components.TabProps) { 88 | @components.Tabs( 89 | components.TabsProps{ 90 | Name: name, 91 | Class: "tabs-border", 92 | ContentClass: "bg-base-100 border-base-300 rounded-box p-4 overflow-x-auto", 93 | Tabs: tabs, 94 | }, 95 | ) 96 | } 97 | -------------------------------------------------------------------------------- /internal/views/pages/server_error.templ: -------------------------------------------------------------------------------- 1 | package pages 2 | 3 | import ( 4 | "fmt" 5 | "github.com/haatos/goshipit/internal" 6 | "github.com/haatos/goshipit/internal/views/components" 7 | ) 8 | 9 | templ InternalServerError() { 10 | @SideNavLayout(nil) { 11 | @SideNavLayout(nil) { 12 | @components.Status( 13 | components.StatusProps{ 14 | Code: 500, 15 | Title: "Internal Server Error", 16 | Description: fmt.Sprintf("Something went terribly wrong! Please contact us at %s if the problem persists.", internal.Settings.ContactEmail), 17 | ReturnButtonLabel: "Go back home", 18 | ReturnButtonAttrs: templ.Attributes{ 19 | "hx-get": "/", 20 | "hx-target": "main", 21 | "hx-swap": "innerHTML", 22 | "hx-push-url": "true", 23 | }, 24 | }, 25 | ) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/views/scripts/copy_button.templ: -------------------------------------------------------------------------------- 1 | package scripts 2 | 3 | templ CodeCopyButtonScript() { 4 | 49 | } 50 | 51 | templ HXCodeCopyButtonScript() { 52 | 95 | } 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@tailwindcss/cli": "^4.0.9", 4 | "@tailwindcss/typography": "^0.5.13", 5 | "daisyui": "^5.0.42", 6 | "tailwindcss": "^4.0.9" 7 | }, 8 | "scripts": { 9 | "build:css": "npx @tailwindcss/cli -i input.css -o ./public/static/css/tw.css --watch" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haatos/goshipit/8a266cfe7140c83927345ab6cb2999dbdba74867/public/favicon.ico -------------------------------------------------------------------------------- /public/static/css/chroma.css: -------------------------------------------------------------------------------- 1 | .bg { 2 | color: #b0c4de; 3 | background-color: #282c34; 4 | } 5 | 6 | .chroma { 7 | color: #b0c4de; 8 | background-color: #111318; 9 | } 10 | 11 | .chroma .lntd { 12 | vertical-align: top; 13 | padding: 0; 14 | margin: 0; 15 | border: 0; 16 | } 17 | 18 | .chroma .lntable { 19 | border-spacing: 0; 20 | padding: 0; 21 | margin: 0; 22 | border: 0; 23 | } 24 | 25 | .chroma .hl { 26 | background-color: #3d4148; 27 | } 28 | 29 | .chroma .lnt { 30 | white-space: pre; 31 | user-select: none; 32 | margin-right: 0.4em; 33 | padding: 0 0.4em 0 0.4em; 34 | color: #58626f; 35 | } 36 | 37 | .chroma .ln { 38 | white-space: pre; 39 | user-select: none; 40 | margin-right: 0.4em; 41 | padding: 0 0.4em 0 0.4em; 42 | color: #58626f; 43 | } 44 | 45 | .chroma .line { 46 | display: flex; 47 | } 48 | 49 | .chroma .k { 50 | color: #76a9f9; 51 | } 52 | 53 | .chroma .kc { 54 | color: #e5c07b; 55 | } 56 | 57 | .chroma .kd { 58 | color: #76a9f9; 59 | } 60 | 61 | .chroma .kn { 62 | color: #76a9f9; 63 | } 64 | 65 | .chroma .kp { 66 | color: #76a9f9; 67 | } 68 | 69 | .chroma .kr { 70 | color: #76a9f9; 71 | } 72 | 73 | .chroma .kt { 74 | color: #e5c07b; 75 | } 76 | 77 | .chroma .n { 78 | color: #aa89ea; 79 | } 80 | 81 | .chroma .na { 82 | color: #cebc3a; 83 | } 84 | 85 | .chroma .nb { 86 | color: #e5c07b; 87 | } 88 | 89 | .chroma .bp { 90 | color: #aa89ea; 91 | } 92 | 93 | .chroma .nc { 94 | color: #ca72ff; 95 | } 96 | 97 | .chroma .no { 98 | color: #aa89ea; 99 | font-weight: bold; 100 | } 101 | 102 | .chroma .nd { 103 | color: #e5c07b; 104 | } 105 | 106 | .chroma .ni { 107 | color: #bda26f; 108 | } 109 | 110 | .chroma .ne { 111 | color: #fd7474; 112 | font-weight: bold; 113 | } 114 | 115 | .chroma .nf { 116 | color: #00b1f7; 117 | } 118 | 119 | .chroma .fm { 120 | color: #aa89ea; 121 | } 122 | 123 | .chroma .nl { 124 | color: #f5a40d; 125 | } 126 | 127 | .chroma .nn { 128 | color: #ca72ff; 129 | } 130 | 131 | .chroma .nx { 132 | color: #aa89ea; 133 | } 134 | 135 | .chroma .py { 136 | color: #cebc3a; 137 | } 138 | 139 | .chroma .nt { 140 | color: #76a9f9; 141 | } 142 | 143 | .chroma .nv { 144 | color: #dcaeea; 145 | } 146 | 147 | .chroma .vc { 148 | color: #dcaeea; 149 | } 150 | 151 | .chroma .vg { 152 | color: #dcaeea; 153 | font-weight: bold; 154 | } 155 | 156 | .chroma .vi { 157 | color: #e06c75; 158 | } 159 | 160 | .chroma .vm { 161 | color: #dcaeea; 162 | } 163 | 164 | .chroma .l { 165 | color: #98c379; 166 | } 167 | 168 | .chroma .ld { 169 | color: #98c379; 170 | } 171 | 172 | .chroma .s { 173 | color: #98c379; 174 | } 175 | 176 | .chroma .sa { 177 | color: #98c379; 178 | } 179 | 180 | .chroma .sb { 181 | color: #98c379; 182 | } 183 | 184 | .chroma .sc { 185 | color: #98c379; 186 | } 187 | 188 | .chroma .dl { 189 | color: #98c379; 190 | } 191 | 192 | .chroma .sd { 193 | color: #7e97c3; 194 | } 195 | 196 | .chroma .s2 { 197 | color: #63c381; 198 | } 199 | 200 | .chroma .se { 201 | color: #d26464; 202 | font-weight: bold; 203 | } 204 | 205 | .chroma .sh { 206 | color: #98c379; 207 | } 208 | 209 | .chroma .si { 210 | color: #98c379; 211 | } 212 | 213 | .chroma .sx { 214 | color: #70b33f; 215 | } 216 | 217 | .chroma .sr { 218 | color: #56b6c2; 219 | } 220 | 221 | .chroma .s1 { 222 | color: #98c379; 223 | } 224 | 225 | .chroma .ss { 226 | color: #56b6c2; 227 | } 228 | 229 | .chroma .m { 230 | color: #d19a66; 231 | } 232 | 233 | .chroma .mb { 234 | color: #d19a66; 235 | } 236 | 237 | .chroma .mf { 238 | color: #d19a66; 239 | } 240 | 241 | .chroma .mh { 242 | color: #d19a66; 243 | } 244 | 245 | .chroma .mi { 246 | color: #d19a66; 247 | } 248 | 249 | .chroma .il { 250 | color: #d19a66; 251 | } 252 | 253 | .chroma .mo { 254 | color: #d19a66; 255 | } 256 | 257 | .chroma .o { 258 | color: #54b1c7; 259 | } 260 | 261 | .chroma .ow { 262 | color: #b756ff; 263 | font-weight: bold; 264 | } 265 | 266 | .chroma .p { 267 | color: #abb2bf; 268 | } 269 | 270 | .chroma .c { 271 | color: #8a93a5; 272 | font-style: italic; 273 | } 274 | 275 | .chroma .ch { 276 | color: #8a93a5; 277 | font-weight: bold; 278 | font-style: italic; 279 | } 280 | 281 | .chroma .cm { 282 | color: #8a93a5; 283 | font-style: italic; 284 | } 285 | 286 | .chroma .c1 { 287 | color: #8a93a5; 288 | font-style: italic; 289 | } 290 | 291 | .chroma .cs { 292 | color: #8a93a5; 293 | font-style: italic; 294 | } 295 | 296 | .chroma .cp { 297 | color: #8a93a5; 298 | font-style: italic; 299 | } 300 | 301 | .chroma .cpf { 302 | color: #8a93a5; 303 | font-style: italic; 304 | } 305 | 306 | .chroma .ge { 307 | font-style: italic; 308 | } 309 | 310 | .chroma .gh { 311 | color: #a2cbff; 312 | font-weight: bold; 313 | } 314 | 315 | .chroma .gi { 316 | color: #a6e22e; 317 | } 318 | 319 | .chroma .go { 320 | color: #a6e22e; 321 | } 322 | 323 | .chroma .gp { 324 | color: #a6e22e; 325 | } 326 | 327 | .chroma .gs { 328 | font-weight: bold; 329 | } 330 | 331 | .chroma .gu { 332 | color: #a2cbff; 333 | } 334 | 335 | .chroma .gt { 336 | color: #a2cbff; 337 | } 338 | 339 | .chroma .gl { 340 | text-decoration: underline; 341 | } 342 | 343 | .chroma { 344 | padding: 5px; 345 | border-radius: 16px; 346 | max-width: 1024px; 347 | overflow-x: auto; 348 | font-size: 14px; 349 | } 350 | 351 | .chroma code { 352 | font-family: "JetBrains Mono", monospace !important; 353 | font-weight: 500 !important; 354 | font-style: normal !important; 355 | } 356 | 357 | @media screen and (min-width: 768px) { 358 | .chroma { 359 | padding: 10px; 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /public/static/css/custom.css: -------------------------------------------------------------------------------- 1 | .font-orbitron { 2 | font-family: "Orbitron", sans-serif; 3 | font-weight: 600; 4 | font-style: normal; 5 | } 6 | 7 | body, 8 | html { 9 | margin: 0; 10 | padding: 0; 11 | width: 100%; 12 | font-family: "Montserrat", sans-serif; 13 | } 14 | 15 | .htmx-indicator { 16 | opacity: 0; 17 | transition: opacity 500ms ease-in; 18 | } 19 | 20 | .htmx-request .htmx-indicator { 21 | opacity: 1 22 | } 23 | 24 | .htmx-request.htmx-indicator { 25 | opacity: 1 26 | } 27 | 28 | article>p>img { 29 | margin: auto; 30 | } 31 | 32 | pre[class=chroma] { 33 | position: relative; 34 | overflow: auto; 35 | 36 | margin: 5px 0; 37 | padding: 1.75rem 0 1.75rem 1rem; 38 | border-radius: 10px; 39 | } 40 | 41 | pre[class=chroma] button { 42 | position: absolute; 43 | top: 5px; 44 | right: 5px; 45 | } 46 | 47 | pre[class=chroma] button:hover { 48 | cursor: pointer; 49 | } 50 | -------------------------------------------------------------------------------- /public/static/images/avatar-reverse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haatos/goshipit/8a266cfe7140c83927345ab6cb2999dbdba74867/public/static/images/avatar-reverse.jpg -------------------------------------------------------------------------------- /public/static/images/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haatos/goshipit/8a266cfe7140c83927345ab6cb2999dbdba74867/public/static/images/avatar.jpg -------------------------------------------------------------------------------- /public/static/images/diff1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haatos/goshipit/8a266cfe7140c83927345ab6cb2999dbdba74867/public/static/images/diff1.png -------------------------------------------------------------------------------- /public/static/images/diff2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haatos/goshipit/8a266cfe7140c83927345ab6cb2999dbdba74867/public/static/images/diff2.png -------------------------------------------------------------------------------- /public/static/images/goshipit-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haatos/goshipit/8a266cfe7140c83927345ab6cb2999dbdba74867/public/static/images/goshipit-logo.png -------------------------------------------------------------------------------- /public/static/images/goshipit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haatos/goshipit/8a266cfe7140c83927345ab6cb2999dbdba74867/public/static/images/goshipit.png -------------------------------------------------------------------------------- /public/static/images/profile-long.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haatos/goshipit/8a266cfe7140c83927345ab6cb2999dbdba74867/public/static/images/profile-long.jpg -------------------------------------------------------------------------------- /public/static/images/profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haatos/goshipit/8a266cfe7140c83927345ab6cb2999dbdba74867/public/static/images/profile.jpg -------------------------------------------------------------------------------- /public/static/images/templ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haatos/goshipit/8a266cfe7140c83927345ab6cb2999dbdba74867/public/static/images/templ.png -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # GoShip.it 2 | 3 | Golang + Templ + HTMX (+ TailwindCSS + DaisyUI) component library to enhance developing an application using the GOTH stack. 4 | 5 | The library contains DaisyUI components translated into Templ components that can be easily customized using both TailwindCSS and DaisyUI. 6 | 7 | Updated to support DaisyUI 5. If you're looking for DaisyUI 4 compatible components, take a look [here](https://old.goship.it) 8 | 9 | ## Getting started 10 | 11 | Install node dependencies: 12 | `npm i -D` 13 | 14 | Build TailwindCSS: 15 | `make tw` 16 | 17 | Generate component code/json, generate templates and run the server: 18 | `make dev` 19 | 20 | ## Code/data generation 21 | 22 | `cmd/generate/main.go` is used to generate JSON, markdown and Go code from source code. JSON is used to store and load component and example component source code to be displayed in HTML. The generator also generates a markdown file that contains up-to-date types for components (from `internal/model/components.go`), and .go file containing a mapping of example names to _templ_ components. 23 | 24 | ## Contributing 25 | 26 | Components are placed in individual .templ files in `internal/views/components/`. The name of the file is used as the name of the component (converted from snake_case to Capitalized Component Name). The .templ file starts with a category name as a comment, e.g. `// data_display`. 27 | 28 | For example 29 | 30 | `internal/views/components/accordion.templ` 31 | 32 | ```go 33 | // data_display 34 | package components 35 | 36 | type AccordionRowProps struct { 37 | Label string 38 | Type string 39 | Name string 40 | } 41 | 42 | templ AccordionRow(props AccordionRowProps) { 43 |
    44 | 45 |
    { label }
    46 |
    47 | { children... } 48 |
    49 |
    50 | } 51 | ``` 52 | 53 | Each component also has an examples file with a corresponding name in `internal/views/examples/`. The file can contain multiple examples, each starting with a comment `// example` with any lines below this line belonging to the example up to the next `// example` line or EOF. E.g.: 54 | 55 | `internal/views/examples/textarea.templ` 56 | 57 | ```go 58 | package examples 59 | 60 | import "github.com/haatos/goshipit/internal/views/components" 61 | 62 | // example 63 | templ BasicTextarea() { 64 |
    65 | @components.Textarea( 66 | components.TextareaProps{ 67 | Label: "Description", 68 | Name: "description", 69 | }, 70 | ) 71 |
    72 | } 73 | 74 | // example 75 | templ BasicTextareaWithError() { 76 |
    77 | @components.Textarea( 78 | components.TextareaProps{ 79 | Label: "Description", 80 | Name: "description", 81 | Err: "Description cannot be empty", 82 | }, 83 | ) 84 |
    85 | } 86 | ``` 87 | 88 | Some examples have corresponding handler functions to provide dummy data for the component to display its usage. E.g. `internal/handler/components.go` contains the handler for lazy-loading example: 89 | 90 | ```go 91 | // LazyLoadExample 92 | func GetLazyLoadExample(c echo.Context) error { 93 | time.Sleep(2 * time.Second) 94 | 95 | return render(c, http.StatusOK, examples.LazyLoadResult()) 96 | } 97 | 98 | // LazyLoadExample 99 | ``` 100 | 101 | The handler must be enclosed with the name of the example component as comment on both sides of the handler's function(s). 102 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /* to disable code block CSS from tailwind/typography, we use another code highlighter */ 2 | const disabledCss = { 3 | "code::before": false, 4 | "code::after": false, 5 | "blockquote p:first-of-type::before": false, 6 | "blockquote p:last-of-type::after": false, 7 | pre: false, 8 | code: false, 9 | "pre code": false, 10 | "code::before": false, 11 | "code::after": false, 12 | }; 13 | 14 | module.exports = { 15 | content: ["internal/views/**/*.templ"], 16 | theme: { 17 | extend: { 18 | /* disable code block CSS */ 19 | typography: { 20 | DEFAULT: { css: disabledCss }, 21 | sm: { css: disabledCss }, 22 | lg: { css: disabledCss }, 23 | xl: { css: disabledCss }, 24 | "2xl": { css: disabledCss }, 25 | }, 26 | }, 27 | }, 28 | }; 29 | --------------------------------------------------------------------------------