Compare commits

..

3 Commits

39 changed files with 347 additions and 96 deletions

View File

@ -16,6 +16,7 @@ const (
type Sources interface { type Sources interface {
ListAll(jwt string, page int) (domain.SourcesResponse, error) ListAll(jwt string, page int) (domain.SourcesResponse, error)
GetById(jwt string, id int64) (domain.SourcesResponse, error) GetById(jwt string, id int64) (domain.SourcesResponse, error)
NewRss(jwt, name, url, sourceType string) (domain.SourcesResponse, error)
} }
type sourceClient struct { type sourceClient struct {
@ -53,7 +54,7 @@ func (c sourceClient) ListAll(jwt string, page int) (domain.SourcesResponse, err
return bind, err return bind, err
} }
if (resp.StatusCode != 200) { if resp.StatusCode != 200 {
return bind, errors.New(bind.Message) return bind, errors.New(bind.Message)
} }
@ -69,9 +70,33 @@ func (c sourceClient) GetById(jwt string, id int64) (domain.SourcesResponse, err
return bind, err return bind, err
} }
if (statusCode != 200) { if statusCode != 200 {
return bind, errors.New(bind.Message) return bind, errors.New(bind.Message)
} }
return bind, nil return bind, nil
} }
func (c sourceClient) NewRss(jwt, name, url, sourceType string) (domain.SourcesResponse, error) {
param := domain.NewSourceParamRequest{
Name: name,
Url: url,
Tags: "",
}
bind := domain.SourcesResponse{}
var endpoint string
if sourceType == domain.SourceCollectorRss {
endpoint = fmt.Sprintf("%s/%s/new/rss", c.serverAddress, SourcesBaseRoute)
}
statusCode, err := PostBodyUrlAuthorized(c.client, endpoint, jwt, param, &bind)
if err != nil {
return bind, err
}
if statusCode != 200 {
return bind, errors.New("got the wrong status code back from the API")
}
return bind, nil
}

View File

@ -58,6 +58,35 @@ func PostUrlAuthorized(client http.Client, endpoint, jwtToken string, t any) err
return nil return nil
} }
func PostBodyUrlAuthorized(client http.Client, endpoint, jwtToken string, body any, t any) (int, error) {
jsonBody, err := json.Marshal(body)
if err != nil {
return 0, err
}
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(jsonBody))
if err != nil {
return 0, err
}
req.Header.Add(HeaderAuthorization, fmt.Sprintf("%s %s", "Bearer", jwtToken))
req.Header.Add(HeaderContentType, ApplicationJson)
//response, err := http.Post(endpoint, ApplicationJson, bytes.NewBuffer(jsonBody))
response, err := client.Do(req)
if err != nil {
return response.StatusCode, err
}
defer response.Body.Close()
decoder := json.NewDecoder(response.Body)
err = decoder.Decode(&t)
if err != nil {
return response.StatusCode, err
}
return response.StatusCode, nil
}
func PostBodyUrl(client http.Client, endpoint string, body any, t any) error { func PostBodyUrl(client http.Client, endpoint string, body any, t any) error {
jsonBody, err := json.Marshal(body) jsonBody, err := json.Marshal(body)
if err != nil { if err != nil {
@ -79,6 +108,22 @@ func PostBodyUrl(client http.Client, endpoint string, body any, t any) error {
return nil return nil
} }
func PostQuery(client http.Client, endpoint string, t any) (int, error) {
response, err := http.Post(endpoint, ApplicationJson, nil)
if err != nil {
return response.StatusCode, err
}
defer response.Body.Close()
decoder := json.NewDecoder(response.Body)
err = decoder.Decode(&t)
if err != nil {
return response.StatusCode, err
}
return response.StatusCode, nil
}
func Get(client http.Client, endpoint, jwt string, t any) (int, error) { func Get(client http.Client, endpoint, jwt string, t any) (int, error) {
req, err := http.NewRequest(http.MethodGet, endpoint, nil) req, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil { if err != nil {

View File

@ -1,21 +1,18 @@
package bulma package bulma
templ Button(color string, isLight, isDark bool) { // This creates a button that accepts children under it.
if isLight { templ Button() {
<button type="button" class={ "button", "is-light", color }> <button type="button" class={ "button" }>
{ children... } { children... }
</button> </button>
} }
if isDark {
<button type="button" class={ "button", "is-dark", color }> // Used to create a button and lets you define the color.
{ children... } // Accepts children.
</button> templ ButtonColor(color string) {
} <button type="button" class={ "button", color }>
if !isLight && !isDark {
<button type="button" class={ "button", color }>
{ children... } { children... }
</button> </button>
}
} }
templ ButtonNewTab(url, text string) { templ ButtonNewTab(url, text string) {
@ -24,7 +21,6 @@ templ ButtonNewTab(url, text string) {
</button> </button>
} }
templ ALink(url, title string) { templ ALink(url, title string) {
<a href={ templ.SafeURL(url) }>{ title }</a> <a href={ templ.SafeURL(url) }>{ title }</a>
} }

View File

@ -0,0 +1,15 @@
package form
templ TextInput(color, id, fieldType, placeholder string) {
if color == "" {
<input class={ "input" } name={ id } id={ id } type={ fieldType } placeholder={ placeholder }/>
} else {
<input class={ "input", color } name={ id } id={ id } type={ fieldType } placeholder={ placeholder }/>
}
}
templ Checkbox(text, id string) {
<label class="checkbox">
<input type="checkbox" id={ id }/> { text }
</label>
}

View File

@ -0,0 +1,13 @@
package form
type NewParam struct {
HxPost string
}
templ New(param NewParam) {
if param.HxPost != "" {
<form hx-post={ param.HxPost }>
{ children... }
</form>
}
}

View File

@ -0,0 +1,33 @@
package form
templ SelectOne(color string, isRound bool) {
if isRound {
<div class="select is-round">
<select>
{ children... }
</select>
</div>
} else {
<div class="select">
<select>
{ children... }
</select>
</div>
}
}
templ SelectOneItem(name string) {
<option>{ name }</option>
}
templ SelectMany(howManySelectable int, color string, isRound bool) {
<div class="select is-multiple">
<select multiple size="{ howManySelectable }">
{ children... }
</select>
</div>
}
templ SelectManyItem(name string) {
<option value={ name }>{ name }</option>
}

View File

@ -0,0 +1,5 @@
package form
templ TextArea(id, placeholder, color string) {
<textarea class={ "textarea", color } id={ id } placeholder={ placeholder }/>
}

View File

@ -0,0 +1,8 @@
package form
const (
InputTypeText = "text"
InputTypePassword = "password"
InputTypeEmail = "email"
InputTypePhoneNumber = "tel"
)

View File

@ -0,0 +1,7 @@
package bulma
templ Notification(message, color string) {
<div class={ "notification", color }>
{ message }
</div>
}

View File

@ -0,0 +1,13 @@
package bulma
templ Tag(message string) {
<span class={ "tag" }>{ message }</span>
}
templ TagColor(message, color string) {
<span class={ "tag", color }>{ message }</span>
}
templ TagColorSize(message, color, size string) {
<span class={ "tag", color, size }>{ message }</span>
}

View File

@ -0,0 +1,49 @@
package bulma
templ Title(message string) {
<h1 class="title">{ message }</h1>
}
templ Subitle(message string) {
<h2 class="subtitle">{ message }</h2>
}
templ H1(message string, isSubtitle bool) {
if isSubtitle {
<h1 class="subtitle is-1">{ message }</h1>
} else {
<h1 class="title is-1">{ message }</h1>
}
}
templ H2(message string, isSubtitle bool) {
if isSubtitle {
<h2 class="subtitle is-2">{ message }</h2>
} else {
<h2 class="title is-2">{ message }</h2>
}
}
templ H3(message string, isSubtitle bool) {
if isSubtitle {
<h3 class="subtitle is-3">{ message }</h3>
} else {
<h3 class="title is-3">{ message }</h3>
}
}
templ H4(message string, isSubtitle bool) {
if isSubtitle {
<h4 class="subtitle is-4">{ message }</h4>
} else {
<h4 class="title is-4">{ message }</h4>
}
}
templ H5(message string, isSubtitle bool) {
if isSubtitle {
<h5 class="subtitle is-5">{ message }</h5>
} else {
<h5 class="title is-5">{ message }</h5>
}
}

View File

@ -7,4 +7,8 @@ const (
ColorWarning = "is-warning" ColorWarning = "is-warning"
ColorSuccess = "is-success" ColorSuccess = "is-success"
ColorError = "is-error" ColorError = "is-error"
SizeNormal = "is-normal"
SizeMedium = "is-medium"
SizeLarge = "is-large"
) )

View File

@ -0,0 +1,5 @@
package templhtml
templ Br(){
<br/>
}

View File

@ -0,0 +1,9 @@
package templhtml
templ ALink(url, title string) {
<a href={ templ.SafeURL(url) }>{ title }</a>
}
templ ANewTab(url, text string) {
<a href={ templ.SafeURL(url) } target="_blank" rel="noopener noreferrer">{ text }</a>
}

2
go.mod
View File

@ -3,7 +3,7 @@ module git.jamestombleson.com/jtom38/newsbot-portal
go 1.22.1 go 1.22.1
require ( require (
git.jamestombleson.com/jtom38/newsbot-api v0.0.0-20240603002809-9237369e5a76 git.jamestombleson.com/jtom38/newsbot-api v0.0.0-20240710142335-56199a795a2b // indirect
github.com/a-h/templ v0.2.747 github.com/a-h/templ v0.2.747
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang-jwt/jwt/v5 v5.2.1
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1

2
go.sum
View File

@ -1,5 +1,7 @@
git.jamestombleson.com/jtom38/newsbot-api v0.0.0-20240603002809-9237369e5a76 h1:B9t5fcfVerMjqnXXPUmYwdmUk76EoEL8x9IRehqg2c4= git.jamestombleson.com/jtom38/newsbot-api v0.0.0-20240603002809-9237369e5a76 h1:B9t5fcfVerMjqnXXPUmYwdmUk76EoEL8x9IRehqg2c4=
git.jamestombleson.com/jtom38/newsbot-api v0.0.0-20240603002809-9237369e5a76/go.mod h1:A3UdJyQ/IEy3utEwJiC4nbi0ohfgrUNRLTei2iZhLLA= git.jamestombleson.com/jtom38/newsbot-api v0.0.0-20240603002809-9237369e5a76/go.mod h1:A3UdJyQ/IEy3utEwJiC4nbi0ohfgrUNRLTei2iZhLLA=
git.jamestombleson.com/jtom38/newsbot-api v0.0.0-20240710142335-56199a795a2b h1:XAXD6OSFDzrJ2O1+wma/vnYfLJdcQZRD6NFjfjxjKv4=
git.jamestombleson.com/jtom38/newsbot-api v0.0.0-20240710142335-56199a795a2b/go.mod h1:A3UdJyQ/IEy3utEwJiC4nbi0ohfgrUNRLTei2iZhLLA=
github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg= github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg=
github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4= github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=

View File

@ -65,6 +65,7 @@ func NewServer(ctx context.Context, configs config.Configs, apiClient apiclient.
//sources.Use(ValidateJwtMiddleware(configs.JwtSecret)) //sources.Use(ValidateJwtMiddleware(configs.JwtSecret))
sources.GET("", s.ListAllSources) sources.GET("", s.ListAllSources)
sources.GET("/add", s.AddSource) sources.GET("/add", s.AddSource)
sources.POST("/add", s.AddSourceAfter)
users := router.Group("/users") users := router.Group("/users")
users.GET("/login", s.UserLogin) users.GET("/login", s.UserLogin)

View File

@ -36,3 +36,13 @@ func (h *Handler) AddSource(c echo.Context) error {
return Render(c, http.StatusOK, sources.Add(models.AddSourcePayloadModel{})) return Render(c, http.StatusOK, sources.Add(models.AddSourcePayloadModel{}))
} }
func (h *Handler) AddSourceAfter(c echo.Context) error {
name := c.FormValue("name")
url := c.FormValue("url")
resp, err := h.api.Sources.NewRss(GetJwtToken(c), name, url, "rss")
if err != nil {
return Render(c, http.StatusOK, sources.AddAfter(err.Error(), true))
}
return Render(c, http.StatusOK, sources.AddAfter(resp.Message, false))
}

View File

@ -1,17 +1,16 @@
package articles package articles
import ( import (
"git.jamestombleson.com/jtom38/newsbot-portal/internal/views/layout" "git.jamestombleson.com/jtom38/newsbot-portal/components/bulma"
"git.jamestombleson.com/jtom38/newsbot-portal/internal/models" "git.jamestombleson.com/jtom38/newsbot-portal/internal/models"
"git.jamestombleson.com/jtom38/newsbot-portal/internal/views/bulma" "git.jamestombleson.com/jtom38/newsbot-portal/internal/views/layout"
) )
templ List(model models.ListArticlesViewModel) { templ List(model models.ListArticlesViewModel) {
@layout.WithTemplate() { @layout.WithTemplate() {
@filterBar() @filterBar()
for _, item := range model.Items { for _, item := range model.Items {
@bulma.ArticleCardWithThumbnail(item.Article.Title, item.Article.Thumbnail, item.Article.Url, item.Article.PubDate.String(), item.Source.DisplayName ) @bulma.ArticleCardWithThumbnail(item.Article.Title, item.Article.Thumbnail, item.Article.Url, item.Article.PubDate.String(), item.Source.DisplayName)
} }
} }
} }

View File

@ -1,9 +0,0 @@
package form
templ Input(color, id, fieldType string) {
if color == "" {
<input class={ "input" } id={ id } type={ fieldType }/>
} else {
<input class={ "input", color } id={ id } type={ fieldType }/>
}
}

View File

@ -1,7 +0,0 @@
package form
templ New(postUrl string) {
<form hx-post={ postUrl }>
{ children... }
</form>
}

View File

@ -1,6 +0,0 @@
package form
const (
InputTypeText = "text"
InputTypePassword = "password"
)

View File

@ -1,7 +1,7 @@
package home package home
import "git.jamestombleson.com/jtom38/newsbot-portal/internal/views/layout" import "git.jamestombleson.com/jtom38/newsbot-portal/internal/views/layout"
import "git.jamestombleson.com/jtom38/newsbot-portal/internal/views/bulma" import "git.jamestombleson.com/jtom38/newsbot-portal/components/bulma"
templ Index() { templ Index() {
@layout.WithTemplate() { @layout.WithTemplate() {

View File

@ -1,18 +1,25 @@
package sources package sources
import ( import (
"git.jamestombleson.com/jtom38/newsbot-portal/components/bulma"
"git.jamestombleson.com/jtom38/newsbot-portal/components/bulma/form"
"git.jamestombleson.com/jtom38/newsbot-portal/internal/models" "git.jamestombleson.com/jtom38/newsbot-portal/internal/models"
"git.jamestombleson.com/jtom38/newsbot-portal/internal/views/bulma"
"git.jamestombleson.com/jtom38/newsbot-portal/internal/views/bulma/form"
"git.jamestombleson.com/jtom38/newsbot-portal/internal/views/layout" "git.jamestombleson.com/jtom38/newsbot-portal/internal/views/layout"
) )
var (
p = form.NewParam{
HxPost: "/sources/add",
}
)
templ Add(model models.AddSourcePayloadModel) { templ Add(model models.AddSourcePayloadModel) {
@layout.WithTemplate() { @layout.WithTemplate() {
<h2>New Source</h2> @bulma.H2("New Source", false)
<p>At this time only direct RSS links are allowed to be provided</p> <p>At this time only direct RSS links are allowed to be provided.</p>
@form.New("/sources/add") { @form.New(p) {
@form.Input("", "url", "text") @form.TextInput("", "name", form.InputTypeText, "Name of the site")
@form.TextInput("", "url", form.InputTypeText, "RSS URL")
@form.Submit("Submit", bulma.ColorPrimary) @form.Submit("Submit", bulma.ColorPrimary)
} }
} }

View File

@ -0,0 +1,11 @@
package sources
import "git.jamestombleson.com/jtom38/newsbot-portal/components/bulma"
templ AddAfter(message string, isError bool) {
if isError {
@bulma.Notification(message, bulma.ColorError)
} else {
@bulma.Notification("The requested source was added to the server", bulma.ColorSuccess)
}
}

View File

@ -1,15 +1,21 @@
package sources package sources
import ( import (
"git.jamestombleson.com/jtom38/newsbot-portal/components/bulma"
templhtml "git.jamestombleson.com/jtom38/newsbot-portal/components/templ-html"
"git.jamestombleson.com/jtom38/newsbot-portal/internal/models" "git.jamestombleson.com/jtom38/newsbot-portal/internal/models"
"git.jamestombleson.com/jtom38/newsbot-portal/internal/views/bulma"
"git.jamestombleson.com/jtom38/newsbot-portal/internal/views/layout" "git.jamestombleson.com/jtom38/newsbot-portal/internal/views/layout"
) )
templ ListAll(model models.ListAllSourcesViewModel) { templ ListAll(model models.ListAllSourcesViewModel) {
@layout.WithTemplate() { @layout.WithTemplate() {
@bulma.Button() {
@templhtml.ALink("/sources/add", "New Source")
}
@templhtml.Br()
@templhtml.Br()
for _, item := range model.Items { for _, item := range model.Items {
@bulma.Button(bulma.ColorPrimary, false, false) { @bulma.ButtonColor(bulma.ColorPrimary) {
@bulma.ANewTab(item.Url, item.DisplayName) @bulma.ANewTab(item.Url, item.DisplayName)
} }
<br/> <br/>

View File

@ -1,17 +1,16 @@
package users package users
import "git.jamestombleson.com/jtom38/newsbot-portal/internal/models" import (
"git.jamestombleson.com/jtom38/newsbot-portal/components/bulma"
"git.jamestombleson.com/jtom38/newsbot-portal/internal/models"
)
// This is returned after the user logs into the application. // This is returned after the user logs into the application.
// It just returns a partial view because it will overlap with the existing template. // It just returns a partial view because it will overlap with the existing template.
templ AfterLogin(vm models.AfterLoginViewModel) { templ AfterLogin(vm models.AfterLoginViewModel) {
if vm.Success { if vm.Success {
<div class="notification is-success"> @bulma.Notification(vm.Message, bulma.ColorSuccess)
{ vm.Message }
</div>
} else { } else {
<div class="notification is-error"> @bulma.Notification(vm.Message, bulma.ColorError)
{ vm.Message }
</div>
} }
} }

View File

@ -1,15 +1,13 @@
package users package users
import "git.jamestombleson.com/jtom38/newsbot-portal/components/bulma"
// This is returned after the user creates an account. // This is returned after the user creates an account.
// It just returns a partial view because it will overlap with the existing template. // It just returns a partial view because it will overlap with the existing template.
templ AfterSignUp(message string, success bool) { templ AfterSignUp(message string, success bool) {
if success { if success {
<div class="notification is-success"> @bulma.Notification(message, bulma.ColorSuccess)
{ message }
</div>
} else { } else {
<div class="notification is-error"> @bulma.Notification(message, bulma.ColorError)
{ message }
</div>
} }
} }

View File

@ -1,23 +1,29 @@
package users package users
import ( import (
"git.jamestombleson.com/jtom38/newsbot-portal/internal/views/bulma/form" "git.jamestombleson.com/jtom38/newsbot-portal/components/bulma/form"
"git.jamestombleson.com/jtom38/newsbot-portal/internal/views/layout" "git.jamestombleson.com/jtom38/newsbot-portal/internal/views/layout"
) )
var (
p = form.NewParam{
HxPost: "/users/login",
}
)
templ LoginNew() { templ LoginNew() {
@layout.WithTemplate() { @layout.WithTemplate() {
@form.New("/users/login") { @form.New(p) {
@form.Field() { @form.Field() {
@form.Label("Username") @form.Label("Username")
@form.Control() { @form.Control() {
@form.Input("", "username", "text") @form.TextInput("", "username", "text", "email address")
} }
} }
@form.Field(){ @form.Field() {
@form.Label("Password") @form.Label("Password")
@form.Control(){ @form.Control() {
@form.Input("", "password", form.InputTypePassword) @form.TextInput("", "password", form.InputTypePassword, "")
} }
} }
@form.Submit("Submit", "is-primary") @form.Submit("Submit", "is-primary")

View File

@ -1,7 +1,7 @@
package users package users
import "git.jamestombleson.com/jtom38/newsbot-portal/internal/views/layout" import "git.jamestombleson.com/jtom38/newsbot-portal/internal/views/layout"
import "git.jamestombleson.com/jtom38/newsbot-portal/internal/views/bulma" import "git.jamestombleson.com/jtom38/newsbot-portal/components/bulma"
templ Logout() { templ Logout() {
@layout.WithTemplate(){ @layout.WithTemplate(){

View File

@ -1,15 +1,19 @@
package users package users
import "git.jamestombleson.com/jtom38/newsbot-portal/internal/views/layout" import (
import "git.jamestombleson.com/jtom38/newsbot-portal/internal/views/bulma" "git.jamestombleson.com/jtom38/newsbot-portal/components/bulma"
templhtml "git.jamestombleson.com/jtom38/newsbot-portal/components/templ-html"
"git.jamestombleson.com/jtom38/newsbot-portal/internal/views/layout"
)
templ Profile() { templ Profile() {
@layout.WithTemplate() { @layout.WithTemplate() {
@bulma.Hero("Profile", "Here you can update your profile 😀") @bulma.Hero("Profile", "Here you can update your profile 😀")
<button type="button" class="button"> @bulma.H2("Sessions", false)
<a href="/users/forcelogout">Logout Everywhere</a> @bulma.Button() {
Logout Everywhere</button> @templhtml.ALink("/users/forcelogout", "Terminate all active sessions")
<p class="subtitle">This will force all active sessions to stop working and require a new login.</p> }
@bulma.Subitle("This will force you to login again as the application will give you a new session value.")
} }
} }

View File

@ -1,23 +1,26 @@
package users package users
import "git.jamestombleson.com/jtom38/newsbot-portal/internal/views/layout" import (
"git.jamestombleson.com/jtom38/newsbot-portal/components/bulma/form"
"git.jamestombleson.com/jtom38/newsbot-portal/internal/views/layout"
)
templ SignUp() { templ SignUp() {
@layout.WithTemplate() { @layout.WithTemplate() {
<form hx-post="/users/signup"> @form.New(form.NewParam{HxPost: "/users/signup"}) {
<div class="field"> @form.Field(){
<label class="label">Username</label> @form.Label("Username")
<div class="control"> @form.Control() {
<input class="input" type="text" name="username" id="username"/> @form.TextInput("", "username", form.InputTypeText, "username or email address")
</div> }
</div> }
<div class="field"> @form.Field() {
<label class="label">Password</label> @form.Label("Password")
<div class="control"> @form.Control() {
<input class="input" type="password" name="password" id="exampleInputPassword1"/> @form.TextInput("", "password", form.InputTypePassword, "Nice strong password, like Ox!")
</div> }
</div> }
<button type="submit" class="button is-primary">Submit</button> @form.Submit("Submit", "")
</form> }
} }
} }