From 81d9e315a0fdd02b6fd93152554b8d7eda1bbf5e Mon Sep 17 00:00:00 2001 From: DutchEllie Date: Tue, 15 Mar 2022 12:48:47 +0100 Subject: [PATCH] Major refactor --- Dockerfile | 4 +- Makefile | 8 +- components/content-view.go | 23 ------ components/homepage.go | 67 ----------------- {components => src}/aboutpage.go | 17 +++-- {components => src}/bannerpanel.go | 2 +- src/block.go | 74 +++++++++++++++++++ {components => src}/galaxiespage.go | 18 +++-- {components => src}/guestbookform.go | 2 +- {components => src}/guestbookpanel.go | 18 +++-- {components => src}/header.go | 2 +- src/homepage.go | 101 ++++++++++++++++++++++++++ {components => src}/homepanel.go | 2 +- src/html-doc.go | 75 +++++++++++++++++++ src/html.go | 63 ++++++++++++++++ src/http.go | 41 +++++++++++ main.go => src/main.go | 9 ++- {components => src}/modal.go | 2 +- {components => src}/navbar.go | 2 +- src/page.go | 61 ++++++++++++++++ {components => src}/updater.go | 2 +- web/blocks/about.html | 22 ++++++ web/blocks/galaxies.html | 46 ++++++++++++ web/blocks/intro.html | 18 +++++ web/static/style.css | 21 ++++++ 25 files changed, 572 insertions(+), 128 deletions(-) delete mode 100644 components/content-view.go delete mode 100644 components/homepage.go rename {components => src}/aboutpage.go (90%) rename {components => src}/bannerpanel.go (90%) create mode 100644 src/block.go rename {components => src}/galaxiespage.go (94%) rename {components => src}/guestbookform.go (99%) rename {components => src}/guestbookpanel.go (89%) rename {components => src}/header.go (91%) create mode 100644 src/homepage.go rename {components => src}/homepanel.go (99%) create mode 100644 src/html-doc.go create mode 100644 src/html.go create mode 100644 src/http.go rename main.go => src/main.go (92%) rename {components => src}/modal.go (97%) rename {components => src}/navbar.go (95%) create mode 100644 src/page.go rename {components => src}/updater.go (97%) create mode 100644 web/blocks/about.html create mode 100644 web/blocks/galaxies.html create mode 100644 web/blocks/intro.html diff --git a/Dockerfile b/Dockerfile index 743ce8b..9f9281a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,8 @@ ARG APIURL WORKDIR /project ADD . /project/ RUN go mod tidy -RUN GOARCH=wasm GOOS=js go build -ldflags="-X 'dutchellie.nl/DutchEllie/proper-website-2/components.ApiURL=$APIURL'" -o web/app.wasm -RUN go build -ldflags="-X 'dutchellie.nl/DutchEllie/proper-website-2/components.ApiURL=$APIURL'" -o app +RUN GOARCH=wasm GOOS=js go build -o web/app.wasm -ldflags="-X 'main.ApiURL=$APIURL'" ./src +RUN go build -o app -ldflags="-X 'main.ApiURL=$APIURL'" ./src FROM alpine:latest AS staging RUN apk --no-cache add ca-certificates diff --git a/Makefile b/Makefile index a300193..7b71ece 100644 --- a/Makefile +++ b/Makefile @@ -2,12 +2,12 @@ APIURL_prod := https://api.nicecock.eu/api/comment APIURL_staging := https://api.nicecock.eu/api/testingcomment build: - GOARCH=wasm GOOS=js go build -ldflags="-X 'dutchellie.nl/DutchEllie/proper-website-2/components.ApiURL=${APIURL_staging}'" -o web/app.wasm - go build -ldflags="-X 'dutchellie.nl/DutchEllie/proper-website-2/components.ApiURL=${APIURL_staging}'" -o app + GOARCH=wasm GOOS=js go build -o web/app.wasm -ldflags="-X 'main.ApiURL=${APIURL_staging}'" ./src + go build -o app -ldflags="-X 'main.ApiURL=${APIURL_staging}'" ./src build-prod: - GOARCH=wasm GOOS=js go build -ldflags="-X 'dutchellie.nl/DutchEllie/proper-website-2/components.ApiURL=${APIURL_prod}'" -o web/app.wasm - go build -ldflags="-X 'dutchellie.nl/DutchEllie/proper-website-2/components.ApiURL=${APIURL_prod}'" -o app + GOARCH=wasm GOOS=js go build -o web/app.wasm -ldflags="-X 'main.ApiURL=${APIURL_prod}'" ./src + go build -o app -ldflags="-X 'main.ApiURL=${APIURL_prod}'" ./src run: build ./app diff --git a/components/content-view.go b/components/content-view.go deleted file mode 100644 index 790248d..0000000 --- a/components/content-view.go +++ /dev/null @@ -1,23 +0,0 @@ -package components - -import ( - "github.com/maxence-charriere/go-app/v9/pkg/app" -) - -type contentView struct { - app.Compo - - panels []app.UI -} - -func newContentView(panels ...app.UI) *contentView { - return &contentView{panels: panels} -} - -func (c *contentView) Render() app.UI { - return app.Div().Body( - app.Range(c.panels).Slice(func(i int) app.UI { - return c.panels[i] - }), - ) -} diff --git a/components/homepage.go b/components/homepage.go deleted file mode 100644 index dec570d..0000000 --- a/components/homepage.go +++ /dev/null @@ -1,67 +0,0 @@ -package components - -import ( - "bytes" - "encoding/json" - "fmt" - "net/http" - - "dutchellie.nl/DutchEllie/proper-website-2/entity" - "github.com/maxence-charriere/go-app/v9/pkg/app" -) - -var ( - ApiURL string -) - -type Homepage struct { - app.Compo - - showGuestbook bool - - page string -} - -func NewHomepage() *Homepage { - return &Homepage{showGuestbook: true, page: "home"} -} - -func (p *Homepage) Render() app.UI { - gbp := newGuestbookPanel() - return app.Div().Body( - &header{}, - &navbar{}, - &homePanel{ - onShowClick: func() { - p.showGuestbook = !p.showGuestbook - }, - }, - &bannerPanel{}, - &guestbookForm{ - OnSubmit: func(name, message string) { - var comment entity.Comment - comment.Name = name - comment.Message = message - - jsondata, err := json.Marshal(comment) - if err != nil { - fmt.Printf("err: %v\n", err) - return - } - url := ApiURL - - req, err := http.Post(url, "application/json", bytes.NewBuffer(jsondata)) - if err != nil { - fmt.Printf("err: %v\n", err) - return - } - if req.StatusCode == 200 { - p.Update() - } - defer req.Body.Close() - }, - }, - //app.If(p.showGuestbook, gbp), - gbp.Render(), - ).Class("main") -} diff --git a/components/aboutpage.go b/src/aboutpage.go similarity index 90% rename from components/aboutpage.go rename to src/aboutpage.go index 31347be..ce0bdaf 100644 --- a/components/aboutpage.go +++ b/src/aboutpage.go @@ -1,4 +1,4 @@ -package components +package main import ( "github.com/maxence-charriere/go-app/v9/pkg/app" @@ -13,11 +13,16 @@ func NewAboutPage() *AboutPage { } func (a *AboutPage) Render() app.UI { - return app.Div().Body( - &header{}, - &navbar{}, - &aboutPanel{}, - ) + return newPage(). + Title("About me"). + LeftBar( + &bannerPanel{}, + ). + Main( + newHTMLBlock(). + Class("right"). + Src("/web/blocks/about.html"), + ) } type aboutPanel struct { diff --git a/components/bannerpanel.go b/src/bannerpanel.go similarity index 90% rename from components/bannerpanel.go rename to src/bannerpanel.go index 848ed8a..8ed8fc1 100644 --- a/components/bannerpanel.go +++ b/src/bannerpanel.go @@ -1,4 +1,4 @@ -package components +package main import "github.com/maxence-charriere/go-app/v9/pkg/app" diff --git a/src/block.go b/src/block.go new file mode 100644 index 0000000..ecb25f1 --- /dev/null +++ b/src/block.go @@ -0,0 +1,74 @@ +package main + +import ( + "github.com/maxence-charriere/go-app/v9/pkg/app" +) + +type htmlBlock struct { + app.Compo + + Iclass string + Isrc string // HTML document source + + // TODO: implement invisibility for other background functions +} + +func newHTMLBlock() *htmlBlock { + return &htmlBlock{} +} + +func (b *htmlBlock) Class(v string) *htmlBlock { + b.Iclass = app.AppendClass(b.Iclass, v) + return b +} + +func (b *htmlBlock) Src(v string) *htmlBlock { + b.Isrc = v + return b +} + +func (b *htmlBlock) Render() app.UI { + return app.Div(). + Class("block"). + Class(b.Iclass). + Body( + newRemoteHTMLDoc(). + Src(b.Isrc), + ) +} + +// ================== +// UI element block +// ================== + +type uiBlock struct { + app.Compo + + Iclass string + Iui []app.UI +} + +func newUIBlock() *uiBlock { + return &uiBlock{} +} + +func (b *uiBlock) Class(v string) *uiBlock { + b.Iclass = app.AppendClass(b.Iclass, v) + return b +} + +func (b *uiBlock) UI(v ...app.UI) *uiBlock { + b.Iui = app.FilterUIElems(v...) + return b +} + +func (b *uiBlock) Render() app.UI { + return app.Div(). + Class("block"). + Class(b.Iclass). + Body( + app.Range(b.Iui).Slice(func(i int) app.UI { + return b.Iui[i] + }), + ) +} diff --git a/components/galaxiespage.go b/src/galaxiespage.go similarity index 94% rename from components/galaxiespage.go rename to src/galaxiespage.go index 9963fa5..623dd30 100644 --- a/components/galaxiespage.go +++ b/src/galaxiespage.go @@ -1,4 +1,4 @@ -package components +package main import "github.com/maxence-charriere/go-app/v9/pkg/app" @@ -11,12 +11,16 @@ func NewGalaxiesPage() *GalaxiesPage { } func (f *GalaxiesPage) Render() app.UI { - return app.Div().Body( - &header{}, - &navbar{}, - &galaxiesPanel{}, - &bannerPanel{}, - ).Class("main") + return newPage(). + Title("Galaxies"). + LeftBar( + &bannerPanel{}, + ). + Main( + newHTMLBlock(). + Class("right"). + Src("/web/blocks/galaxies.html"), + ) } type galaxiesPanel struct { diff --git a/components/guestbookform.go b/src/guestbookform.go similarity index 99% rename from components/guestbookform.go rename to src/guestbookform.go index c029c0c..f27a840 100644 --- a/components/guestbookform.go +++ b/src/guestbookform.go @@ -1,4 +1,4 @@ -package components +package main import ( "fmt" diff --git a/components/guestbookpanel.go b/src/guestbookpanel.go similarity index 89% rename from components/guestbookpanel.go rename to src/guestbookpanel.go index d08b88c..9605c1c 100644 --- a/components/guestbookpanel.go +++ b/src/guestbookpanel.go @@ -1,4 +1,4 @@ -package components +package main import ( "encoding/json" @@ -35,13 +35,15 @@ func newGuestbookPanel() *guestbookPanel { } func (g *guestbookPanel) Render() app.UI { - return app.Div().Body( - app.Range(g.comments).Slice(func(i int) app.UI { - return &guestbookComment{ - Comment: g.comments[i], - } - }), - ).Class("content gbp") + return newUIBlock(). + Class("right"). + UI( + app.Range(g.comments).Slice(func(i int) app.UI { + return &guestbookComment{ + Comment: g.comments[i], + } + }), + ) } func (g *guestbookPanel) LoadComments() { diff --git a/components/header.go b/src/header.go similarity index 91% rename from components/header.go rename to src/header.go index c0d23c7..af6edd7 100644 --- a/components/header.go +++ b/src/header.go @@ -1,4 +1,4 @@ -package components +package main import "github.com/maxence-charriere/go-app/v9/pkg/app" diff --git a/src/homepage.go b/src/homepage.go new file mode 100644 index 0000000..2488ac9 --- /dev/null +++ b/src/homepage.go @@ -0,0 +1,101 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + + "dutchellie.nl/DutchEllie/proper-website-2/entity" + "github.com/maxence-charriere/go-app/v9/pkg/app" +) + +var ( + ApiURL string +) + +type Homepage struct { + app.Compo + + showGuestbook bool +} + +func NewHomepage() *Homepage { + return &Homepage{} +} + +func (p *Homepage) Render() app.UI { + gbp := newGuestbookPanel() + return newPage(). + Title("Homepage"). + LeftBar( + &bannerPanel{}, + ). + Main( + newHTMLBlock(). + Class("right"). + Src("/web/blocks/intro.html"), + &guestbookForm{ + OnSubmit: func(name, message string) { + var comment entity.Comment + comment.Name = name + comment.Message = message + + jsondata, err := json.Marshal(comment) + if err != nil { + fmt.Printf("err: %v\n", err) + return + } + url := ApiURL + + req, err := http.Post(url, "application/json", bytes.NewBuffer(jsondata)) + if err != nil { + fmt.Printf("err: %v\n", err) + return + } + if req.StatusCode == 200 { + p.Update() + } + defer req.Body.Close() + }, + }, + gbp.Render(), + ) + /* + return app.Div().Body( + &header{}, + &navbar{}, + &homePanel{ + onShowClick: func() { + p.showGuestbook = !p.showGuestbook + }, + }, + &bannerPanel{}, + &guestbookForm{ + OnSubmit: func(name, message string) { + var comment entity.Comment + comment.Name = name + comment.Message = message + + jsondata, err := json.Marshal(comment) + if err != nil { + fmt.Printf("err: %v\n", err) + return + } + url := ApiURL + + req, err := http.Post(url, "application/json", bytes.NewBuffer(jsondata)) + if err != nil { + fmt.Printf("err: %v\n", err) + return + } + if req.StatusCode == 200 { + p.Update() + } + defer req.Body.Close() + }, + }, + //app.If(p.showGuestbook, gbp), + gbp.Render(), + ).Class("main")*/ +} diff --git a/components/homepanel.go b/src/homepanel.go similarity index 99% rename from components/homepanel.go rename to src/homepanel.go index e7ce3f3..6b1650e 100644 --- a/components/homepanel.go +++ b/src/homepanel.go @@ -1,4 +1,4 @@ -package components +package main import "github.com/maxence-charriere/go-app/v9/pkg/app" diff --git a/src/html-doc.go b/src/html-doc.go new file mode 100644 index 0000000..6200a49 --- /dev/null +++ b/src/html-doc.go @@ -0,0 +1,75 @@ +package main + +import ( + "fmt" + + "github.com/maxence-charriere/go-app/v9/pkg/app" +) + +type htmlDoc struct { + app.Compo + + Ihtml string +} + +func newHTMLDoc() *htmlDoc { + return &htmlDoc{} +} + +func (h *htmlDoc) HTML(v string) *htmlDoc { + h.Ihtml = fmt.Sprintf("
%s
", v) + return h +} + +func (h *htmlDoc) Render() app.UI { + return app.Raw(h.Ihtml) +} + +type remoteHTMLDoc struct { + app.Compo + + Isrc string + + html htmlContent +} + +func newRemoteHTMLDoc() *remoteHTMLDoc { + return &remoteHTMLDoc{} +} + +func (h *remoteHTMLDoc) Src(v string) *remoteHTMLDoc { + h.Isrc = v + return h +} + +func (h *remoteHTMLDoc) OnMount(ctx app.Context) { + h.load(ctx) +} + +func (h *remoteHTMLDoc) OnNav(ctx app.Context) { + h.load(ctx) +} + +func (h *remoteHTMLDoc) load(ctx app.Context) { + src := h.Isrc + ctx.ObserveState(htmlState(src)). + While(func() bool { + return src == h.Isrc + }). + OnChange(func() { + + }). + Value(&h.html) + + ctx.NewAction(getHTML, app.T("path", h.Isrc)) +} + +func (h *remoteHTMLDoc) Render() app.UI { + return app.Div(). + Body( + app.If(h.html.Status == loaded, + newHTMLDoc(). + HTML(h.html.Data), + ).Else(), + ) +} diff --git a/src/html.go b/src/html.go new file mode 100644 index 0000000..a361e72 --- /dev/null +++ b/src/html.go @@ -0,0 +1,63 @@ +package main + +import ( + "errors" + + "github.com/maxence-charriere/go-app/v9/pkg/app" +) + +const ( + getHTML = "/html/get" +) + +func handleGetHTML(ctx app.Context, a app.Action) { + path := a.Tags.Get("path") + if path == "" { + app.Log(errors.New("getting html failed")) + return + } + + state := htmlState(path) + + var ht htmlContent + ctx.GetState(state, &ht) + switch ht.Status { + case loading, loaded: + return + } + + ht.Status = loading + ht.Error = nil + ctx.SetState(state, ht) + + res, err := get(ctx, path) + if err != nil { + ht.Status = loadingErr + ht.Error = err + ctx.SetState(state, ht) + return + } + + ht.Status = loaded + ht.Data = string(res) + ctx.SetState(state, ht) +} + +func htmlState(src string) string { + return src +} + +type htmlContent struct { + Status status + Error error + Data string +} + +type status int + +const ( + neverLoaded status = iota + loading + loadingErr + loaded +) diff --git a/src/http.go b/src/http.go new file mode 100644 index 0000000..dc05002 --- /dev/null +++ b/src/http.go @@ -0,0 +1,41 @@ +package main + +import ( + "fmt" + "io" + "net/http" + "strings" + + "github.com/maxence-charriere/go-app/v9/pkg/app" +) + +func get(ctx app.Context, path string) ([]byte, error) { + url := path + if !strings.HasPrefix(url, "http") { + u := ctx.Page().URL() + u.Path = path + url = u.String() + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + fmt.Printf("Error at getting html page\n") + return nil, err + } + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + // Which means either client or server error + if res.StatusCode >= 400 { + return nil, err + } + + b, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + return b, nil +} diff --git a/main.go b/src/main.go similarity index 92% rename from main.go rename to src/main.go index 1c9cd99..513aa2a 100644 --- a/main.go +++ b/src/main.go @@ -4,7 +4,6 @@ import ( "log" "net/http" - "dutchellie.nl/DutchEllie/proper-website-2/components" "github.com/maxence-charriere/go-app/v9/pkg/app" ) @@ -19,13 +18,15 @@ const ( ) func main() { - homepage := components.NewHomepage() - aboutpage := components.NewAboutPage() - galaxiespage := components.NewGalaxiesPage() + homepage := NewHomepage() + aboutpage := NewAboutPage() + galaxiespage := NewGalaxiesPage() app.Route("/", homepage) app.Route("/about", aboutpage) app.Route("/galaxies", galaxiespage) + app.Handle(getHTML, handleGetHTML) + // This is executed on the client side only. // It handles client side stuff // It exits immediately on the server side diff --git a/components/modal.go b/src/modal.go similarity index 97% rename from components/modal.go rename to src/modal.go index 1c7e21a..a200266 100644 --- a/components/modal.go +++ b/src/modal.go @@ -1,4 +1,4 @@ -package components +package main import "github.com/maxence-charriere/go-app/v9/pkg/app" diff --git a/components/navbar.go b/src/navbar.go similarity index 95% rename from components/navbar.go rename to src/navbar.go index 6bbea67..8317fd8 100644 --- a/components/navbar.go +++ b/src/navbar.go @@ -1,4 +1,4 @@ -package components +package main import "github.com/maxence-charriere/go-app/v9/pkg/app" diff --git a/src/page.go b/src/page.go new file mode 100644 index 0000000..f7856e4 --- /dev/null +++ b/src/page.go @@ -0,0 +1,61 @@ +package main + +import "github.com/maxence-charriere/go-app/v9/pkg/app" + +// Page is a generic page. By default it has a header, navbar and a default leftbar +type page struct { + app.Compo + + Ititle string + /*Description + Blah blah + etc*/ + + IleftBar []app.UI + Imain []app.UI + + // TODO: Possibly add "updateavailable" here, so it shows up on every page +} + +func newPage() *page { + return &page{} +} + +func (p *page) Title(t string) *page { + p.Ititle = t + return p +} + +func (p *page) LeftBar(v ...app.UI) *page { + p.IleftBar = app.FilterUIElems(v...) + return p +} + +func (p *page) Main(v ...app.UI) *page { + p.Imain = app.FilterUIElems(v...) + return p +} + +func (p *page) Render() app.UI { + return app.Div(). + Class("main"). + Body( + // Header and navbar + &header{}, + app.Div(). + Class("left"). + Body( + &navbar{}, + app.Range(p.IleftBar).Slice(func(i int) app.UI { + return p.IleftBar[i] + }), + ), + app.Div(). + Class("right"). + Body( + app.Range(p.Imain).Slice(func(i int) app.UI { + return p.Imain[i] + }), + ), + ) +} diff --git a/components/updater.go b/src/updater.go similarity index 97% rename from components/updater.go rename to src/updater.go index d5f32b9..c721ef4 100644 --- a/components/updater.go +++ b/src/updater.go @@ -1,4 +1,4 @@ -package components +package main import "github.com/maxence-charriere/go-app/v9/pkg/app" diff --git a/web/blocks/about.html b/web/blocks/about.html new file mode 100644 index 0000000..d5649c1 --- /dev/null +++ b/web/blocks/about.html @@ -0,0 +1,22 @@ + +

I am a 21 year old computer science student (they/them, he/him, she/her), living and studying in + The Netherlands. I like Docker, Kubernetes and Golang! +
+ I made this website because I was inspired again by the amazing Neocities pages that I discovered because of my + friends. + They also have their own pages (you can find them on the friends tab, do check them out!) and I just had to get a + good website of my own! +
+ I am not that great at web development, especially design, but I love trying it regardless! +

+ To say a bit more about me personally, I love all things computers. From servers to embedded devices! I love the + cloud and all that it brings + (except for big megacorps, but alright) and it's my goal to work for a big cloud company! +
+ Aside from career path ambitions,ボーカロイドはすきです! I love vocaloid and other Japanese music and culture!! + I also like Vtubers, especially from Hololive and it's my goal to one day finally understand them in their native + language! +

+ There is a lot more to say in words, but who cares about those! Have a look around my creative digital oasis and see + what crazy stuff you can find! +

\ No newline at end of file diff --git a/web/blocks/galaxies.html b/web/blocks/galaxies.html new file mode 100644 index 0000000..2498137 --- /dev/null +++ b/web/blocks/galaxies.html @@ -0,0 +1,46 @@ +

+ Galaxies +

+

+ Here you can find some really really really cool pages that I found on the internet. + Some of these are blogs or even blogposts I found, but the ones on top are special! + They're the websites of friends of mine! Please visit them, because they worked really hard + on their websites as well! +

+
+

My friends!

+ +
+
+

Neat webspaces

+

Just very neat websites I found. Not necessarily by people I know. + I just thought it would be nice to share them here!

+ +
\ No newline at end of file diff --git a/web/blocks/intro.html b/web/blocks/intro.html new file mode 100644 index 0000000..2aa37db --- /dev/null +++ b/web/blocks/intro.html @@ -0,0 +1,18 @@ +

Welcome, internet surfer!

+
+

Please sign my guestbook

+ +
+ +

+Welcome to my webspace! Whether you stumbled across this page by accident +or were linked here, you're more than welcome! This is my personal project that I like +to work on! I was inspired by a couple friends of mine, please do check their webspaces +out as well under "Galaxies" on the left side there! +If you like this page, there is a lot more, so have a look around! You can also leave a +nice message for me in the guestbook! There is no registration (unlike the rest of the "modern" +internet) so nothing of that sort! +That said, this website is my creative outlet and a way to introduce myself, so be kind please! +Also its code is entirely open-source and can be found +here so if you like that sort +of stuff, be my guest it's cool!

\ No newline at end of file diff --git a/web/static/style.css b/web/static/style.css index 4789913..130731f 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -46,6 +46,16 @@ body { color:rgb(252, 230, 255) } +.left { + float:left; + max-width: 250px; +} + +.right { + float:right; + max-width: 614px; +} + .leftbar { border: 3px solid; border-radius: 4px; @@ -57,6 +67,17 @@ body { padding: 5px 0px; } +.block { + border: 3px solid; + border-radius: 4px; + border-color: rgb(252, 230, 255); + background-color: rgb(54, 39, 48); + margin-bottom: 5px; + position: relative; + width: 614px; + padding: 10px; +} + .content { border: 3px solid; border-radius: 4px;