From 9f02d355bca343d58c9fd7251a9ae78639e207b7 Mon Sep 17 00:00:00 2001 From: Santi Lertsumran Date: Sat, 5 Oct 2024 22:45:44 +0700 Subject: [PATCH] feat: add invate and nav bar --- priv/static/app.css | 13 -- priv/static/nav.css | 308 +++++++++++++++++++++++++++++++ src/app/components/nav.gleam | 119 ++++++++++++ src/app/pages.gleam | 5 + src/app/pages/home.gleam | 26 +-- src/app/pages/invite.gleam | 70 +++++++ src/app/pages/layout.gleam | 11 +- src/app/router.gleam | 16 ++ src/app/routes/user_routes.gleam | 4 + 9 files changed, 534 insertions(+), 38 deletions(-) create mode 100644 priv/static/nav.css create mode 100644 src/app/components/nav.gleam create mode 100644 src/app/pages/invite.gleam diff --git a/priv/static/app.css b/priv/static/app.css index 2f8c4db..82a870f 100644 --- a/priv/static/app.css +++ b/priv/static/app.css @@ -820,11 +820,6 @@ select { background-color: rgb(21 128 61 / var(--tw-bg-opacity)); } -.bg-indigo-500 { - --tw-bg-opacity: 1; - background-color: rgb(99 102 241 / var(--tw-bg-opacity)); -} - .bg-indigo-600 { --tw-bg-opacity: 1; background-color: rgb(79 70 229 / var(--tw-bg-opacity)); @@ -839,10 +834,6 @@ select { background-color: rgb(255 255 255 / .05); } -.p-1 { - padding: 0.25rem; -} - .px-3 { padding-left: 0.75rem; padding-right: 0.75rem; @@ -873,10 +864,6 @@ select { padding-bottom: 3rem; } -.pr-1 { - padding-right: 0.25rem; -} - .text-center { text-align: center; } diff --git a/priv/static/nav.css b/priv/static/nav.css new file mode 100644 index 0000000..b1d52ae --- /dev/null +++ b/priv/static/nav.css @@ -0,0 +1,308 @@ +:root { + --background: #9c88ff; + --navbar-width: 256px; + --navbar-width-min: 80px; + --navbar-dark-primary: #18283b; + --navbar-dark-secondary: #2c3e50; + --navbar-light-primary: #f5f6fa; + --navbar-light-secondary: #8392a5; +} + +#nav-toggle:checked ~ #nav-header { + width: calc(var(--navbar-width-min) - 16px); +} + +#nav-toggle:checked ~ #nav-content, +#nav-toggle:checked ~ #nav-footer { + width: var(--navbar-width-min); +} + +#nav-toggle:checked ~ #nav-header #nav-title { + opacity: 0; + pointer-events: none; + transition: opacity .1s; +} + +#nav-toggle:checked ~ #nav-header label[for="nav-toggle"] { + left: calc(50% - 8px); + transform: translate(-50%); +} + +#nav-toggle:checked ~ #nav-header #nav-toggle-burger { + background: var(--navbar-light-primary); +} + +#nav-toggle:checked ~ #nav-header #nav-toggle-burger:before, +#nav-toggle:checked ~ #nav-header #nav-toggle-burger:after { + width: 16px; + background: var(--navbar-light-secondary); + transform: translate(0, 0) rotate(0deg); +} + +#nav-toggle:checked ~ #nav-content .nav-button button { + opacity: 0; + transition: opacity .1s; +} + +#nav-toggle:checked ~ #nav-content .nav-button .fas { + min-width: calc(100% - 16px); +} + +#nav-toggle:checked ~ #nav-footer #nav-footer-avatar { + margin-left: 0; + left: 50%; + transform: translate(-50%); +} + +#nav-toggle:checked ~ #nav-footer #nav-footer-titlebox, +#nav-toggle:checked ~ #nav-footer label[for="nav-footer-toggle"] { + opacity: 0; + transition: opacity .1s; + pointer-events: none; +} + +#nav-bar { + z-index: 5; + position: absolute; + left: 1vw; + top: 1vw; + height: calc(100% - 2vw); + background: var(--navbar-dark-primary); + border-radius: 16px; + display: flex; + flex-direction: column; + color: var(--navbar-light-primary); + font-family: Verdana, Geneva, Tahoma, sans-serif; + overflow: hidden; + user-select: none; +} + +#nav-bar hr { + margin: 0; + position: relative; + left: 16px; + width: calc(100% - 32px); + border: none; + border-top: solid 1px var(--navbar-dark-secondary); +} + +#nav-bar a { + color: inherit; + text-decoration: inherit; +} + +#nav-bar input[type="checkbox"] { + display: none; +} + +#nav-header { + position: relative; + width: var(--navbar-width); + left: 16px; + width: calc(var(--navbar-width) - 16px); + min-height: 80px; + background: var(--navbar-dark-primary); + border-radius: 16px; + z-index: 2; + display: flex; + align-items: center; + transition: width .2s; +} + +#nav-header hr { + position: absolute; + bottom: 0; +} + +#nav-title { + font-size: 1rem; + transition: opacity 1s; +} + +label[for="nav-toggle"] { + position: absolute; + right: 0; + width: 3rem; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +#nav-toggle-burger { + position: relative; + width: 16px; + height: 2px; + background: var(--navbar-dark-primary); + border-radius: 99px; + transition: background .2s; +} + +#nav-toggle-burger:before, +#nav-toggle-burger:after { + content: ''; + position: absolute; + top: -6px; + width: 10px; + height: 2px; + background: var(--navbar-light-primary); + border-radius: 99px; + transform: translate(2px, 8px) rotate(30deg); + transition: .2s; +} + +#nav-toggle-burger:after { + top: 6px; + transform: translate(2px, -8px) rotate(-30deg); +} + +#nav-content { + margin: -16px 0; + padding: 16px 0; + position: relative; + flex: 1; + width: var(--navbar-width); + background: var(--navbar-dark-primary); + box-shadow: 0 0 0 16px var(--navbar-dark-primary); + direction: rtl; + overflow-x: hidden; + transition: width .2s; +} + +#nav-content::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +#nav-content::-webkit-scrollbar-thumb { + border-radius: 99px; + background-color: #D62929; +} + +#nav-content::-webkit-scrollbar-button { + height: 16px; +} + +.nav-button { + position: relative; + margin-left: 16px; + height: 54px; + display: flex; + align-items: center; + color: var(--navbar-light-secondary); + direction: ltr; + cursor: pointer; + z-index: 1; + transition: color .2s; +} + +.nav-button button { + transition: opacity 1s; +} + +.nav-button .fas { + transition: min-width .2s; +} + +.nav-button:hover { + color: var(--background); +} + +#nav-bar .fas { + min-width: 3rem; + text-align: center; +} + +#nav-footer { + position: relative; + width: var(--navbar-width); + height: 54px; + background: var(--navbar-dark-secondary); + border-radius: 16px; + display: flex; + flex-direction: column; + z-index: 2; + transition: width .2s, height .2s; +} + +#nav-footer-heading { + position: relative; + width: 100%; + height: 54px; + display: flex; + align-items: center; +} + +#nav-footer-avatar { + position: relative; + margin: 11px 0 11px 16px; + left: 0; + width: 32px; + height: 32px; + border-radius: 50%; + overflow: hidden; + transform: translate(0); + transition: .2s; +} + +#nav-footer-avatar img { + height: 100%; +} + +#nav-footer-titlebox { + position: relative; + margin-left: 16px; + width: 10px; + display: flex; + flex-direction: column; + transition: opacity 1s; +} + +#nav-footer-subtitle { + color: var(--navbar-light-secondary); + font-size: .6rem; +} + +#nav-toggle:not(:checked) ~ #nav-footer-toggle:checked + #nav-footer { + height: 30%; + min-height: 54px; + + label[for="nav-footer-toggle"] { + transform: rotate(180deg); + } +} + +label[for="nav-footer-toggle"] { + position: absolute; + right: 0; + width: 3rem; + height: 100%; + display: flex; + align-items: center; + cursor: pointer; + transition: transform .2s, opacity .2s; +} + +#nav-footer-content { + margin: 0 16px 16px 16px; + border-top: solid 1px var(--navbar-light-secondary); + padding: 16px 0; + color: var(--navbar-light-secondary); + font-size: .8rem; + overflow: auto; + + &::-webkit-scrollbar { + width: 8px; + height: 8px; + } + + &::-webkit-scrollbar-thumb { + border-radius: 99px; + background-color: #D62929; + } +} + +.nav-bar-padding-space { + padding-left: var(--navbar-width-min); +} \ No newline at end of file diff --git a/src/app/components/nav.gleam b/src/app/components/nav.gleam new file mode 100644 index 0000000..f0e18de --- /dev/null +++ b/src/app/components/nav.gleam @@ -0,0 +1,119 @@ +import lustre/attribute +import lustre/element +import lustre/element/html + +pub fn nav_bar() -> element.Element(t) { + html.div([attribute.id("nav-bar")], [ + nav_toggle(), + nav_header(), + nav_content(), + nav_footer_toggle(), + nav_footer(), + ]) +} + +fn nav_toggle() -> element.Element(t) { + html.input([ + attribute.id("nav-toggle"), + attribute.type_("checkbox"), + attribute.checked(True), + ]) +} + +fn nav_header() -> element.Element(t) { + html.div([attribute.id("nav-header")], [ + html.a( + [ + attribute.id("nav-title"), + attribute.href("https://codepen.io"), + attribute.target("_blank"), + ], + [ + element.text("CR"), + html.i([attribute.class("fa-brands fa-accessible-icon")], []), + element.text("PPY "), + html.i([attribute.class("fa-solid fa-person-chalkboard")], []), + ], + ), + html.label([attribute.for("nav-toggle")], [ + html.span([attribute.id("nav-toggle-burger")], []), + ]), + html.hr([]), + ]) +} + +fn nav_content() -> element.Element(t) { + html.div([attribute.id("nav-content")], [ + nav_button("fas fa-solid fa-user-plus", "Invite", "GET", "/invite"), + // nav_button("fas fa-images", "Assets", "GET", "#"), + // nav_button("fas fa-thumbtack", "Pinned Items", "GET", "#"), + html.hr([]), + // nav_button("fas fa-heart", "Following", "GET", "#"), + nav_button("fas fa-chart-line", "Trending", "GET", "#"), + // nav_button("fas fa-fire", "Challenges", "GET", "#"), + // nav_button("fas fa-magic", "Spark", "GET", "#"), + nav_button("fas fa-solid fa-gear", "Setting", "GET", "#"), + html.hr([]), + nav_button( + "fas fa-solid fa-arrow-right-from-bracket", + "Sign Out", + "POST", + "/signout", + ), + ]) +} + +fn nav_button( + icon_class: String, + text: String, + method: String, + link: String, +) -> element.Element(t) { + html.div([attribute.class("nav-button")], [ + html.i([attribute.class(icon_class)], []), + html.form([attribute.method(method), attribute.action(link)], [ + html.button([], [element.text(text)]), + ]), + ]) +} + +fn nav_footer_toggle() -> element.Element(t) { + html.input([attribute.id("nav-footer-toggle"), attribute.type_("checkbox")]) +} + +fn nav_footer() -> element.Element(t) { + html.div([attribute.id("nav-footer")], [ + nav_footer_heading(), + nav_footer_content(), + ]) +} + +fn nav_footer_heading() -> element.Element(t) { + html.div([attribute.id("nav-footer-heading")], [ + html.div([attribute.id("nav-footer-avatar")], [ + html.img([ + attribute.src( + "https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp&f=y", + ), + ]), + ]), + html.div([attribute.id("nav-footer-titlebox")], [ + html.a( + [ + attribute.id("nav-footer-title"), + attribute.href("/profile"), + attribute.target("_blank"), + ], + [element.text("unknown")], + ), + html.span([attribute.id("nav-footer-subtitle")], [element.text("User")]), + ]), + html.label([attribute.for("nav-footer-toggle")], [ + html.i([attribute.class("fas fa-caret-up")], []), + ]), + ]) +} + +fn nav_footer_content() -> element.Element(t) { + html.div([attribute.id("nav-footer-content")], [element.text("")]) +} diff --git a/src/app/pages.gleam b/src/app/pages.gleam index 2d5580f..ab3f13c 100644 --- a/src/app/pages.gleam +++ b/src/app/pages.gleam @@ -1,6 +1,7 @@ import app/models/item.{type Item} import app/pages/forgot_password import app/pages/home +import app/pages/invite import app/pages/reset_password import app/pages/signin import app/pages/signup @@ -34,3 +35,7 @@ pub fn submit_forgot_password() { pub fn reset_password(token: String, error: String) { reset_password.root(token, error) } + +pub fn invite(error: String) { + invite.root(error) +} diff --git a/src/app/pages/home.gleam b/src/app/pages/home.gleam index 6e054d4..92651b0 100644 --- a/src/app/pages/home.gleam +++ b/src/app/pages/home.gleam @@ -1,3 +1,4 @@ +import app/components/nav import app/helpers/uuid import app/models/item.{ type Item, item_status_to_string, next_status, prev_status, @@ -13,29 +14,8 @@ import lustre/element/html.{button, form, svg, textarea} import lustre/element/svg pub fn root(board_id: String, items: List(Item)) -> Element(t) { - html.div([attribute.class("flex flex-col")], [ - html.div([attribute.class("flex justify-between m-1.5")], [ - html.h2([], [element.text("Crappy Board")]), - html.div([attribute.class("flex flex-row")], [ - form( - [ - attribute.class("pr-1"), - attribute.method("GET"), - attribute.action("/invite"), - ], - [ - button([attribute.class("bg-indigo-500 rounded p-1")], [ - element.text("Invite"), - ]), - ], - ), - form([attribute.method("POST"), attribute.action("/signout")], [ - button([attribute.class("bg-indigo-500 rounded p-1")], [ - element.text("Sign out"), - ]), - ]), - ]), - ]), + html.div([attribute.class("flex flex-col nav-bar-padding-space")], [ + html.div([], [nav.nav_bar()]), html.div([attribute.class("flex")], [ html.div([attribute.class("flex-1")], [ html.div( diff --git a/src/app/pages/invite.gleam b/src/app/pages/invite.gleam new file mode 100644 index 0000000..97663cd --- /dev/null +++ b/src/app/pages/invite.gleam @@ -0,0 +1,70 @@ +import lustre/attribute.{attribute} +import lustre/element.{type Element} +import lustre/element/html + +pub fn root(error: String) -> Element(t) { + html.div( + [ + attribute.class( + "flex min-h-full flex-col justify-center px-6 py-12 lg:px-8", + ), + ], + [ + html.div([attribute.class("sm:mx-auto sm:w-full sm:max-w-sm")], [ + html.h2( + [ + attribute.class( + "mt-10 text-center text-2xl font-bold leading-9 tracking-tight", + ), + ], + [element.text("Invite to your friend")], + ), + ]), + html.div([attribute.class("mt-10 sm:mx-auto sm:w-full sm:max-w-sm")], [ + html.form( + [ + attribute.class("space-y-6"), + attribute.method("POST"), + attribute.action("/invite"), + ], + [ + html.div([], [ + html.label( + [ + attribute.for("email"), + attribute.class("block text-sm font-medium leading-6"), + ], + [element.text("Email address")], + ), + html.div([attribute.class("mt-2")], [ + html.input([ + attribute.class( + "block w-full bg-white/[.05] rounded-md border-0 py-1.5 shadow-sm ring-1 ring-inset ring-white/[.1] placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6", + ), + attribute.id("email"), + attribute.name("email"), + attribute.type_("email"), + ]), + ]), + ]), + html.div([], [ + html.button( + [ + attribute.type_("sumbit"), + attribute.class( + "flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600", + ), + ], + [element.text("Submit")], + ), + ]), + html.div( + [attribute.class("text-center text-red-600 font-bold text-lg")], + [element.text(error)], + ), + ], + ), + ]), + ], + ) +} diff --git a/src/app/pages/layout.gleam b/src/app/pages/layout.gleam index 8f83d10..0a1a155 100644 --- a/src/app/pages/layout.gleam +++ b/src/app/pages/layout.gleam @@ -1,8 +1,8 @@ import lustre/attribute -import lustre/element.{type Element} +import lustre/element import lustre/element/html -pub fn layout(elements: List(Element(t))) -> Element(t) { +pub fn layout(elements: List(element.Element(t))) -> element.Element(t) { html.html([], [ html.head([], [ html.title([], "Crappy Board"), @@ -11,6 +11,13 @@ pub fn layout(elements: List(Element(t))) -> Element(t) { attribute.attribute("content", "width=device-width, initial-scale=1"), ]), html.link([attribute.rel("stylesheet"), attribute.href("/static/app.css")]), + html.link([attribute.rel("stylesheet"), attribute.href("/static/nav.css")]), + html.link([ + attribute.rel("stylesheet"), + attribute.href( + "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css", + ), + ]), ]), html.body([attribute.class("bg-gray-900")], elements), ]) diff --git a/src/app/router.gleam b/src/app/router.gleam index ec71e05..c61b843 100644 --- a/src/app/router.gleam +++ b/src/app/router.gleam @@ -39,6 +39,7 @@ pub fn handle_request(req: Request, ctx: Context) -> Response { use <- wisp.require_method(req, http.Get) user_routes.activate_user(req, ctx) } + ["invite"] -> invite(req, ctx) ["boards", board_id, "items", "create"] -> { use ctx <- web.authenticate(req, ctx) use ctx <- web.authorized(req, ctx) @@ -163,3 +164,18 @@ fn get_reset_password_form(req: Request) -> Response { } } } + +fn invite(req: Request, ctx: Context) -> Response { + case req.method { + http.Get -> get_invite_form() + http.Post -> user_routes.post_invite(req, ctx) + _ -> wisp.method_not_allowed([http.Get, http.Post]) + } +} + +fn get_invite_form() -> Response { + [pages.invite("")] + |> layout + |> element.to_document_string_builder + |> wisp.html_response(200) +} diff --git a/src/app/routes/user_routes.gleam b/src/app/routes/user_routes.gleam index 560504b..3cb0972 100644 --- a/src/app/routes/user_routes.gleam +++ b/src/app/routes/user_routes.gleam @@ -336,3 +336,7 @@ pub fn post_reset_password(req: Request, ctx: Context) { } } } + +pub fn post_invite(req: Request, ctx: Context) { + todo +}