first drafts for inventory sync page
This commit is contained in:
@@ -28,5 +28,5 @@ func (s *Server) createAccount(r *http.Request) (response.Response, error) {
|
||||
return nil, response.Errorf("failed to create account: %w", err)
|
||||
}
|
||||
|
||||
return response.SeeOther(fmt.Sprintf("/accounts/%d", acct.ID)), nil
|
||||
return response.SeeOther(fmt.Sprintf("/accounts/%d", acct.AccountID)), nil
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ func authorizeByMatchingAccountID(r *http.Request, acctIDPathPosition int) error
|
||||
}
|
||||
|
||||
id := getIdentity(r.Context())
|
||||
if id.Account == nil || id.Account.ID != acctID {
|
||||
if id.Account == nil || id.Account.AccountID != acctID {
|
||||
return response.Unauthorized().
|
||||
Msgf("user does not have access to account %d", acctID)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func (s *Server) login(r *http.Request) (response.Response, error) {
|
||||
return nil, response.Errorf("failed to create account: %w", err)
|
||||
}
|
||||
|
||||
return response.SeeOther(fmt.Sprintf("/accounts/%d", acct.ID)), nil
|
||||
return response.SeeOther(fmt.Sprintf("/accounts/%d", acct.AccountID)), nil
|
||||
}
|
||||
|
||||
// GET /login/callback
|
||||
|
||||
@@ -42,6 +42,10 @@ func (b bodyRes) Redirect(code redirect.Code, to string) Response {
|
||||
return Redirect(code, to).wrap(b)
|
||||
}
|
||||
|
||||
func (b bodyRes) HTML(body []byte) Response {
|
||||
return HTML(body).wrap(b)
|
||||
}
|
||||
|
||||
func (b bodyRes) JSON(body any) Response {
|
||||
return JSON(body).wrap(b)
|
||||
}
|
||||
@@ -82,3 +86,11 @@ func (b bodyRes) getCookies() []http.Cookie {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b bodyRes) getContentType() (contentType string, ok bool) {
|
||||
if b.res != nil {
|
||||
return b.res.getContentType()
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
@@ -42,6 +42,10 @@ func (c cookieRes) Redirect(code redirect.Code, to string) Response {
|
||||
return Redirect(code, to).wrap(c)
|
||||
}
|
||||
|
||||
func (c cookieRes) HTML(body []byte) Response {
|
||||
return HTML(body).wrap(c)
|
||||
}
|
||||
|
||||
func (c cookieRes) JSON(body any) Response {
|
||||
return JSON(body).wrap(c)
|
||||
}
|
||||
@@ -85,3 +89,11 @@ func (c cookieRes) getCookies() []http.Cookie {
|
||||
|
||||
return []http.Cookie{c.cookie}
|
||||
}
|
||||
|
||||
func (c cookieRes) getContentType() (contentType string, ok bool) {
|
||||
if c.res != nil {
|
||||
return c.res.getContentType()
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"ruben/inventory2/internal/site/redirect"
|
||||
)
|
||||
|
||||
type (
|
||||
htmlRes struct {
|
||||
body []byte
|
||||
res Response
|
||||
}
|
||||
)
|
||||
|
||||
var _ Response = htmlRes{}
|
||||
|
||||
func HTML(body []byte) Response {
|
||||
return htmlRes{
|
||||
body: body,
|
||||
}
|
||||
}
|
||||
|
||||
func (h htmlRes) String() string {
|
||||
if h.res != nil {
|
||||
return fmt.Sprintf(`{"body": %q, "nested": %s}`, string(h.body), h.res)
|
||||
}
|
||||
return fmt.Sprintf(`{"body": %q}`, h.body)
|
||||
}
|
||||
|
||||
func (h htmlRes) wrap(res Response) Response {
|
||||
h.res = res
|
||||
return h
|
||||
}
|
||||
|
||||
func (h htmlRes) Status(code int) Response {
|
||||
return Status(code).wrap(h)
|
||||
}
|
||||
|
||||
func (h htmlRes) Redirect(code redirect.Code, to string) Response {
|
||||
return Redirect(code, to).wrap(h)
|
||||
}
|
||||
|
||||
func (h htmlRes) HTML(body []byte) Response {
|
||||
h.body = body
|
||||
return h
|
||||
}
|
||||
|
||||
func (h htmlRes) JSON(body any) Response {
|
||||
return JSON(body).wrap(h)
|
||||
}
|
||||
|
||||
func (h htmlRes) Body(body io.ReadCloser) Response {
|
||||
return Body(body).wrap(h)
|
||||
}
|
||||
|
||||
func (h htmlRes) Cookie(ck http.Cookie) Response {
|
||||
return Cookie(ck).wrap(h)
|
||||
}
|
||||
|
||||
func (h htmlRes) getStatus() (int, bool) {
|
||||
if h.res == nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return h.res.getStatus()
|
||||
}
|
||||
|
||||
func (h htmlRes) getRedirect() (code redirect.Code, to string, ok bool) {
|
||||
if h.res == nil {
|
||||
return 0, "", false
|
||||
}
|
||||
|
||||
return h.res.getRedirect()
|
||||
}
|
||||
|
||||
func (h htmlRes) getBody() (body io.ReadCloser, ok bool, err error) {
|
||||
return io.NopCloser(bytes.NewBuffer(h.body)), true, nil
|
||||
}
|
||||
|
||||
func (h htmlRes) getCookies() []http.Cookie {
|
||||
if h.res != nil {
|
||||
return h.res.getCookies()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h htmlRes) getContentType() (contentType string, ok bool) {
|
||||
return "text/html", true
|
||||
}
|
||||
@@ -44,6 +44,10 @@ func (j jsonRes) Redirect(code redirect.Code, to string) Response {
|
||||
return Redirect(code, to).wrap(j)
|
||||
}
|
||||
|
||||
func (j jsonRes) HTML(body []byte) Response {
|
||||
return HTML(body).wrap(j)
|
||||
}
|
||||
|
||||
func (j jsonRes) JSON(body any) Response {
|
||||
j.body = body
|
||||
return j
|
||||
@@ -85,3 +89,7 @@ func (j jsonRes) getCookies() []http.Cookie {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j jsonRes) getContentType() (contentType string, ok bool) {
|
||||
return "application/json", true
|
||||
}
|
||||
|
||||
@@ -83,6 +83,10 @@ func (r redirectRes) Redirect(code redirect.Code, to string) Response {
|
||||
return r
|
||||
}
|
||||
|
||||
func (r redirectRes) HTML(body []byte) Response {
|
||||
return HTML(body).wrap(r)
|
||||
}
|
||||
|
||||
func (r redirectRes) JSON(body any) Response {
|
||||
return JSON(body).wrap(r)
|
||||
}
|
||||
@@ -122,3 +126,11 @@ func (r redirectRes) getCookies() []http.Cookie {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r redirectRes) getContentType() (contentType string, ok bool) {
|
||||
if r.res != nil {
|
||||
return r.res.getContentType()
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ type (
|
||||
Status(int) Response
|
||||
Redirect(code redirect.Code, to string) Response
|
||||
Body(io.ReadCloser) Response
|
||||
HTML([]byte) Response
|
||||
JSON(any) Response
|
||||
Cookie(http.Cookie) Response
|
||||
|
||||
@@ -19,6 +20,7 @@ type (
|
||||
getBody() (body io.ReadCloser, ok bool, err error)
|
||||
getRedirect() (code redirect.Code, to string, ok bool)
|
||||
getCookies() []http.Cookie
|
||||
getContentType() (contentType string, ok bool)
|
||||
|
||||
wrap(Response) Response
|
||||
}
|
||||
|
||||
@@ -44,6 +44,10 @@ func (s statusRes) Redirect(code redirect.Code, to string) Response {
|
||||
return Redirect(code, to).wrap(s)
|
||||
}
|
||||
|
||||
func (s statusRes) HTML(body []byte) Response {
|
||||
return HTML(body).wrap(s)
|
||||
}
|
||||
|
||||
func (s statusRes) JSON(body any) Response {
|
||||
return JSON(body).wrap(s)
|
||||
}
|
||||
@@ -83,3 +87,11 @@ func (s statusRes) getCookies() []http.Cookie {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s statusRes) getContentType() (contentType string, ok bool) {
|
||||
if s.res != nil {
|
||||
return s.res.getContentType()
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@ func Write(w http.ResponseWriter, r *http.Request, res Response) {
|
||||
hdrs.Add("Set-Cookie", ck.String())
|
||||
}
|
||||
|
||||
if ct, ok := res.getContentType(); ok {
|
||||
hdrs.Add("Content-Type", ct)
|
||||
}
|
||||
|
||||
if code, to, ok := res.getRedirect(); ok {
|
||||
http.Redirect(w, r, to, code.Int())
|
||||
return
|
||||
@@ -44,7 +48,7 @@ func Write(w http.ResponseWriter, r *http.Request, res Response) {
|
||||
}
|
||||
|
||||
func WriteError(w http.ResponseWriter, err error) {
|
||||
var status int
|
||||
status := http.StatusInternalServerError
|
||||
|
||||
if e, ok := GetError(err); ok {
|
||||
if status, ok = e.GetStatus(); !ok {
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
:root {
|
||||
--border-thin: 1px;
|
||||
--border-medium: 2px;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@@ -19,16 +24,19 @@
|
||||
}
|
||||
|
||||
select {
|
||||
border-width: 2px;
|
||||
border-width: var(--border-medium);
|
||||
border-color: var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.border-thin {
|
||||
border-width: 1px;
|
||||
border-width: var(--border-thin);
|
||||
}
|
||||
.border-medium {
|
||||
border-width: var(--border-medium);
|
||||
}
|
||||
.border-button {
|
||||
border-width: 2px;
|
||||
border-width: var(--border-medium);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
@layer components {
|
||||
table {
|
||||
display: block; /* fixes scrolling when in a flexbox */
|
||||
border-collapse: collapse;
|
||||
|
||||
thead {
|
||||
border-color: var(--table-border);
|
||||
border-width: var(--border-thin) var(--border-thin) 0 var(--border-thin);
|
||||
|
||||
& > tr {
|
||||
border-color: var(--table-body-border);
|
||||
|
||||
& > :is(th, td) {
|
||||
border-color: var(--table-body-border);
|
||||
background-color: var(--table-thead);
|
||||
}
|
||||
}
|
||||
|
||||
& + tbody {
|
||||
border-top-width: var(--border-thin);
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
border-color: var(--table-body-border) var(--table-border);
|
||||
border-left-width: var(--border-thin);
|
||||
border-right-width: var(--border-thin);
|
||||
border-bottom-width: 0;
|
||||
|
||||
& > tr {
|
||||
border-color: var(--table-border);
|
||||
|
||||
& > :is(th, td) {
|
||||
border-color: var(--table-border);
|
||||
background-color: var(--table-tbody);
|
||||
}
|
||||
}
|
||||
|
||||
&:has(+ tfoot) {
|
||||
border-bottom-width: var(--border-thin);
|
||||
}
|
||||
}
|
||||
tfoot {
|
||||
border-color: var(--table-border);
|
||||
border-width: 0 var(--border-thin) var(--border-thin) var(--border-thin);
|
||||
|
||||
& > tr {
|
||||
border-color: var(--table-body-border);
|
||||
|
||||
& > :is(th, td) {
|
||||
border-color: var(--table-body-border);
|
||||
background-color: var(--table-tfoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-width: 0 0 var(--border-thin) 0;
|
||||
|
||||
&:last-child {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
th, td {
|
||||
border-width: 0 var(--border-thin) 0 0;
|
||||
|
||||
&:last-child {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
--text-3xl: 1.875rem;
|
||||
--text-4xl: 2.25rem;
|
||||
--text-5xl: 3rem;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
--radius-lg: var(--radius);
|
||||
--default-font-family: var(--font-sans);
|
||||
@@ -180,6 +181,9 @@
|
||||
.mt-\[3em\] {
|
||||
margin-top: 3em;
|
||||
}
|
||||
.mb-\[0\.5em\] {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.mb-\[1em\] {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
@@ -195,18 +199,27 @@
|
||||
.min-h-full {
|
||||
min-height: 100%;
|
||||
}
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
.max-w-\[50\%\] {
|
||||
max-width: 50%;
|
||||
}
|
||||
.max-w-full {
|
||||
max-width: 100%;
|
||||
}
|
||||
.min-w-fit {
|
||||
min-width: fit-content;
|
||||
}
|
||||
.grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.basis-full {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -231,6 +244,9 @@
|
||||
.rounded-lg {
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.rounded-sm {
|
||||
border-radius: calc(var(--radius) - 4px);
|
||||
}
|
||||
.border-\[1px\] {
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 1px;
|
||||
@@ -244,6 +260,9 @@
|
||||
.bg-card {
|
||||
background-color: var(--card);
|
||||
}
|
||||
.p-\[0\.5em\] {
|
||||
padding: 0.5em;
|
||||
}
|
||||
.p-\[1em\] {
|
||||
padding: 1em;
|
||||
}
|
||||
@@ -271,9 +290,30 @@
|
||||
--tw-font-weight: var(--font-weight-bold);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
.font-semibold {
|
||||
--tw-font-weight: var(--font-weight-semibold);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
.text-nowrap {
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.hover\:bg-accent {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:underline {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@layer base {
|
||||
select {
|
||||
@@ -355,6 +395,12 @@
|
||||
--sidebar-accent-foreground: var(--base-800);
|
||||
--sidebar-border: var(--base-200);
|
||||
--sidebar-ring: var(--primary-300);
|
||||
--table-thead: var(--base-400);
|
||||
--table-tbody: var(--base-200);
|
||||
--table-tfoot: var(--base-400);
|
||||
--table-border: var(--base-300);
|
||||
--table-body-border: var(--base-500);
|
||||
--table-inner-border: var(--base-300);
|
||||
--display-color: var(--foreground);
|
||||
--text-color: var(--foreground);
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@@ -389,10 +435,18 @@
|
||||
--sidebar-accent-foreground: var(--base-200);
|
||||
--sidebar-border: var(--base-800);
|
||||
--sidebar-ring: var(--primary-300);
|
||||
--table-thead: var(--base-600);
|
||||
--table-tbody: var(--base-800);
|
||||
--table-tfoot: var(--base-600);
|
||||
--table-border: var(--base-700);
|
||||
--table-body-border: var(--base-500);
|
||||
--table-inner-border: var(--base-700);
|
||||
}
|
||||
}
|
||||
* {
|
||||
color: var(--foreground);
|
||||
@layer theme {
|
||||
* {
|
||||
color: var(--foreground);
|
||||
}
|
||||
}
|
||||
.dark {
|
||||
--background: var(--base-950);
|
||||
@@ -477,6 +531,10 @@
|
||||
background-color: var(--sidebar-accent);
|
||||
}
|
||||
}
|
||||
:root {
|
||||
--border-thin: 1px;
|
||||
--border-medium: 2px;
|
||||
}
|
||||
@layer base {
|
||||
header, footer {
|
||||
border-color: var(--sidebar-border);
|
||||
@@ -488,18 +546,83 @@
|
||||
border-top-width: 1px;
|
||||
}
|
||||
select {
|
||||
border-width: 2px;
|
||||
border-width: var(--border-medium);
|
||||
border-color: var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
.border-thin {
|
||||
border-width: 1px;
|
||||
border-width: var(--border-thin);
|
||||
}
|
||||
.border-medium {
|
||||
border-width: var(--border-medium);
|
||||
}
|
||||
.border-button {
|
||||
border-width: 2px;
|
||||
border-width: var(--border-medium);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
}
|
||||
@layer components {
|
||||
table {
|
||||
display: block;
|
||||
border-collapse: collapse;
|
||||
thead {
|
||||
border-color: var(--table-border);
|
||||
border-width: var(--border-thin) var(--border-thin) 0 var(--border-thin);
|
||||
& > tr {
|
||||
border-color: var(--table-body-border);
|
||||
& > :is(th, td) {
|
||||
border-color: var(--table-body-border);
|
||||
background-color: var(--table-thead);
|
||||
}
|
||||
}
|
||||
& + tbody {
|
||||
border-top-width: var(--border-thin);
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
border-color: var(--table-body-border) var(--table-border);
|
||||
border-left-width: var(--border-thin);
|
||||
border-right-width: var(--border-thin);
|
||||
border-bottom-width: 0;
|
||||
& > tr {
|
||||
border-color: var(--table-border);
|
||||
& > :is(th, td) {
|
||||
border-color: var(--table-border);
|
||||
background-color: var(--table-tbody);
|
||||
}
|
||||
}
|
||||
&:has(+ tfoot) {
|
||||
border-bottom-width: var(--border-thin);
|
||||
}
|
||||
}
|
||||
tfoot {
|
||||
border-color: var(--table-border);
|
||||
border-width: 0 var(--border-thin) var(--border-thin) var(--border-thin);
|
||||
& > tr {
|
||||
border-color: var(--table-body-border);
|
||||
& > :is(th, td) {
|
||||
border-color: var(--table-body-border);
|
||||
background-color: var(--table-tfoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
th, td {
|
||||
padding: 0.5em;
|
||||
}
|
||||
tr {
|
||||
border-width: 0 0 var(--border-thin) 0;
|
||||
&:last-child {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
th, td {
|
||||
border-width: 0 var(--border-thin) 0 0;
|
||||
&:last-child {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@property --tw-border-style {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
|
||||
@@ -14,3 +14,4 @@
|
||||
@import "./interactivity.css";
|
||||
@import "./svg.css";
|
||||
@import "./accessibility.css";
|
||||
@import "./components.css";
|
||||
|
||||
@@ -80,6 +80,13 @@
|
||||
--sidebar-border: var(--base-200);
|
||||
--sidebar-ring: var(--primary-300);
|
||||
|
||||
--table-thead: var(--base-400);
|
||||
--table-tbody: var(--base-200);
|
||||
--table-tfoot: var(--base-400);
|
||||
--table-border: var(--base-300);
|
||||
--table-body-border: var(--base-500);
|
||||
--table-inner-border: var(--base-300);
|
||||
|
||||
--display-color: var(--foreground);
|
||||
--text-color: var(--foreground);
|
||||
|
||||
@@ -115,11 +122,20 @@
|
||||
--sidebar-accent-foreground: var(--base-200);
|
||||
--sidebar-border: var(--base-800);
|
||||
--sidebar-ring: var(--primary-300);
|
||||
|
||||
--table-thead: var(--base-600);
|
||||
--table-tbody: var(--base-800);
|
||||
--table-tfoot: var(--base-600);
|
||||
--table-border: var(--base-700);
|
||||
--table-body-border: var(--base-500);
|
||||
--table-inner-border: var(--base-700);
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
color: var(--foreground);
|
||||
@layer theme {
|
||||
* {
|
||||
color: var(--foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
||||
+30
-16
@@ -15,11 +15,33 @@ import (
|
||||
)
|
||||
|
||||
// GET /
|
||||
// compiles the page template or component template matching the url
|
||||
func (s *Server) serveTemplates(r *http.Request) (response.Response, error) {
|
||||
ctx := r.Context()
|
||||
name, pathParams := getPageTemplateNameForURL(r.URL)
|
||||
name, args := s.getTemplateNameAndArgs(r, "internal/site/templates/component_bodies")
|
||||
|
||||
templateArgs := []any{
|
||||
b, err := s.templater.ExecuteComponentBody(name, args...)
|
||||
if err == nil {
|
||||
return response.HTML(b), nil
|
||||
}
|
||||
|
||||
if !isFileNotFoundError(err) {
|
||||
return s.handleTemplateError(err, args...)
|
||||
}
|
||||
|
||||
name, args = s.getTemplateNameAndArgs(r, "internal/site/templates/page_bodies")
|
||||
|
||||
if b, err = s.templater.ExecutePage(name, args...); err == nil {
|
||||
return response.HTML(b), nil
|
||||
}
|
||||
|
||||
return s.handleTemplateError(err, args...)
|
||||
}
|
||||
|
||||
func (s *Server) getTemplateNameAndArgs(r *http.Request, templateDir string) (name string, args []any) {
|
||||
ctx := r.Context()
|
||||
name, pathParams := getTemplateNameForURL(r.URL, templateDir)
|
||||
|
||||
return name, []any{
|
||||
"Request",
|
||||
r,
|
||||
// add services and data here
|
||||
@@ -47,22 +69,15 @@ func (s *Server) serveTemplates(r *http.Request) (response.Response, error) {
|
||||
"Auth",
|
||||
newTemplateAuthenticator(r),
|
||||
}
|
||||
|
||||
b, err := s.templater.ExecutePage(name, templateArgs...)
|
||||
if err != nil {
|
||||
return s.handleTemplateError(err, templateArgs...)
|
||||
}
|
||||
|
||||
return response.Body(io.NopCloser(bytes.NewBuffer(b))), nil
|
||||
}
|
||||
|
||||
// TODO: clean this up...
|
||||
// TODO: somehow tell what the path params are and pass them up.
|
||||
// - then consider pushing this functionality into the template library.
|
||||
//
|
||||
// getPageTemplateNameForURL eliminate any trailing .html or /, and checks for any
|
||||
// getComponentTemplateNameForURL eliminate any trailing .html or /, and checks for any
|
||||
// file with path parameters in the name, eg '{abc}.html.tmpl', prefering exact filename matches.
|
||||
func getPageTemplateNameForURL(u *url.URL) (name string, params map[string]string) {
|
||||
func getTemplateNameForURL(u *url.URL, templateDir string) (name string, params map[string]string) {
|
||||
fp := strings.TrimPrefix(strings.TrimSuffix(strings.TrimSuffix(u.Path, ".html"), "/"), "/")
|
||||
if fp == "" {
|
||||
// "/" maps to "/index"
|
||||
@@ -72,17 +87,16 @@ func getPageTemplateNameForURL(u *url.URL) (name string, params map[string]strin
|
||||
fpParts := strings.Split(fp, "/")
|
||||
res := getMatchingGlobPatternsCapturingFilepathIncludingParametrizedFilepaths(fpParts)
|
||||
for _, combs := range res {
|
||||
const pageBodiesPrefix = "internal/site/templates/page_bodies"
|
||||
pattern := path.Join(pageBodiesPrefix, path.Join(combs...)) + ".html.tmpl"
|
||||
pattern := path.Join(templateDir, path.Join(combs...)) + ".html.tmpl"
|
||||
|
||||
matches, _ := filepath.Glob(pattern)
|
||||
if len(matches) == 0 {
|
||||
pattern := path.Join(pageBodiesPrefix, path.Join(combs...), "index") + ".html.tmpl"
|
||||
pattern := path.Join(templateDir, path.Join(combs...), "index") + ".html.tmpl"
|
||||
matches, _ = filepath.Glob(pattern)
|
||||
}
|
||||
if len(matches) > 0 {
|
||||
match := matches[0]
|
||||
name = strings.TrimPrefix(strings.TrimSuffix(match, ".html.tmpl"), pageBodiesPrefix+"/")
|
||||
name = strings.TrimPrefix(strings.TrimSuffix(match, ".html.tmpl"), templateDir+"/")
|
||||
|
||||
patternParts := strings.Split(name, "/")
|
||||
params = make(map[string]string)
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
{{/* .Identity.Account.AccountID | .PathParams.acctID | .AccountID */}}
|
||||
|
||||
{{- $dot := or .dot . }}
|
||||
{{- .Auth.ByMatchingAccountID 2 }}
|
||||
|
||||
|
||||
|
||||
{{- $acctID := 0 }}
|
||||
{{- if $dot.Identity }}
|
||||
{{- $acctID = $dot.Identity.Account.AccountID }}
|
||||
{{- else if .PathParams }}
|
||||
{{- $acctID = $dot.PathParams.acctID | parseInt64 }}
|
||||
{{- else }}
|
||||
{{- $acctID = $dot.AccountID }}
|
||||
{{- end }}
|
||||
{{- $stores := $dot.Accounts.GetShops $acctID -}}
|
||||
|
||||
|
||||
<table id="inventory-table-2" class="max-w-full overflow-x-scroll">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Shop
|
||||
</th>
|
||||
<th>
|
||||
Listing
|
||||
</th>
|
||||
<th>
|
||||
SKU
|
||||
</th>
|
||||
<th>
|
||||
Description
|
||||
</th>
|
||||
<th>
|
||||
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{/* TODO: list existing row */}}
|
||||
{{- componentBody "accounts/{acctID}/inventory/sync-table-v2/new-row" "dot" $dot }}
|
||||
{{/* TODO: save the saved rows */}}
|
||||
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th>
|
||||
Shop
|
||||
</th>
|
||||
<th>
|
||||
Listing
|
||||
</th>
|
||||
<th>
|
||||
SKU
|
||||
</th>
|
||||
<th>
|
||||
Description
|
||||
</th>
|
||||
<th>
|
||||
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
{{/* .Identity.Account.AccountID, .Accounts */}}
|
||||
|
||||
{{- $dot := or .dot . -}}
|
||||
|
||||
|
||||
{{- $acctID := $dot.Identity.Account.AccountID }}
|
||||
{{- $shops := $dot.Accounts.GetShops $acctID -}}
|
||||
|
||||
|
||||
<tr _="
|
||||
on setListing(listing)
|
||||
set the innerHTML of (the last <td[data-id=sku]/> in me) to listing.sku
|
||||
set the innerHTML of (the last <td[data-id=description]/> in me) to listing.description
|
||||
-- TODO: only remove if there isn't already a 'new row'
|
||||
remove @disabled from (the <button/> in last <td[data-id=save]/> in me)
|
||||
on resetListing
|
||||
set the innerHTML of (the last <td[data-id=sku]/> in me) to '-'
|
||||
set the innerHTML of (the last <td[data-id=description]/> in me) to '-'
|
||||
add @disabled to (the <button/> in last <td[data-id=save]/> in me)
|
||||
"
|
||||
>
|
||||
<td>
|
||||
<select
|
||||
class="min-w-fit cursor-pointer"
|
||||
hx-get="/accounts/{{$acctID}}/platforms/{platform}/shops/{shopID}/listing-select"
|
||||
hx-vals='js:{
|
||||
platform: event.target.value.replace(/-[^ ]*/, ""),
|
||||
shopID: event.target.value.replace(/[^ ]*-/, "")
|
||||
}'
|
||||
hx-target="next td"
|
||||
_="
|
||||
on change
|
||||
log event.target.value
|
||||
send resetListing() to closest <tr/>
|
||||
"
|
||||
>
|
||||
<option disabled selected>- Shops -</option>
|
||||
{{- range $shop := $shops }}
|
||||
<option value="{{$shop.Platform}}-{{$shop.ShopID}}">
|
||||
{{ $shop.Name }} - {{$shop.Platform}} - {{$shop.ShopID}}
|
||||
</option>
|
||||
{{- end }}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
-
|
||||
</td>
|
||||
<td data-id="sku">
|
||||
-
|
||||
</td>
|
||||
<td data-id="description">
|
||||
-
|
||||
</td>
|
||||
<td data-id="save">
|
||||
<button
|
||||
disabled
|
||||
hx-get="/accounts/{{$acctID}}/inventory/sync-table-v2/new-row"
|
||||
hx-target="closest tr"
|
||||
hx-swap="afterend"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
{{- $dot := or .dot . }}
|
||||
|
||||
{{- $dot.Auth.ByMatchingAccountID 2 }}
|
||||
|
||||
{{- $acctID := $dot.Identity.Account.AccountID }}
|
||||
{{- $stores := $dot.Accounts.GetShops $acctID -}}
|
||||
|
||||
|
||||
<table id="inventory-table" class="max-w-full overflow-x-scroll">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
{{- range $store := $stores }}
|
||||
<th class="text-nowrap">
|
||||
{{ $store.Platform }}: {{ $store.Name }}
|
||||
</th>
|
||||
<th class="text-nowrap">
|
||||
Count
|
||||
</th>
|
||||
{{- end }}
|
||||
|
||||
<th>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{- componentBody "accounts/{acctID}/inventory/sync-table/new-row" "dot" $dot }}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
|
||||
{{- range $store := $stores }}
|
||||
<th class="text-nowrap">
|
||||
{{ $store.Platform }}: {{ $store.Name }}
|
||||
</th>
|
||||
<th class="text-nowrap">
|
||||
Count
|
||||
</th>
|
||||
{{- end }}
|
||||
|
||||
<th>
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
{{- $dot := or .dot .}}
|
||||
|
||||
{{- $dot.Auth.ByMatchingAccountID 2 }}
|
||||
|
||||
{{- $acctID := $dot.Identity.Account.AccountID }}
|
||||
{{- $shops := $dot.Accounts.GetShops $acctID -}}
|
||||
|
||||
|
||||
<tr>
|
||||
{{- range $shop := $shops }}
|
||||
{{- $listings := $dot.Accounts.GetListingsForShop $acctID $shop.ShopID }}
|
||||
<td>
|
||||
<select
|
||||
class="min-w-fit cursor-pointer"
|
||||
_="
|
||||
on change
|
||||
set dataset to the dataset of the first of my selectedOptions
|
||||
|
||||
set listingShowcase to the <#listing-showcase/>
|
||||
if <#listing-showcase/> then
|
||||
send displayListing(
|
||||
id: dataset.id,
|
||||
name: dataset.name,
|
||||
description: dataset.description,
|
||||
count: dataset.count
|
||||
) to listingShowcase
|
||||
end
|
||||
|
||||
set the innerHTML of the next <td/> to the dataset.count
|
||||
"
|
||||
>
|
||||
<option disabled selected>- Listings -</option>
|
||||
{{- range $listing := $listings }}
|
||||
<option
|
||||
value="{{$listing.ListingID}}"
|
||||
data-id="{{$listing.ListingID}}"
|
||||
data-name="{{$listing.Name}}"
|
||||
data-description="{{$listing.Description}}"
|
||||
data-count="{{$listing.Count}}"
|
||||
>
|
||||
{{ $listing.Name }}
|
||||
</option>
|
||||
{{- end }}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
{{- end }}
|
||||
<td>
|
||||
<button class="cursor-pointer underline w-full">
|
||||
Save
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<table class="max-w-full"
|
||||
id="listing-showcase"
|
||||
_="
|
||||
on displayListing(id, name, description, count)
|
||||
set innerHTML of <#listing-showcase-id/> to id
|
||||
set innerHTML of <#listing-showcase-name/> to name
|
||||
set innerHTML of <#listing-showcase-description/> to description
|
||||
set innerHTML of <#listing-showcase-count/> to count
|
||||
"
|
||||
>
|
||||
<thead>
|
||||
<th>
|
||||
ID
|
||||
</th>
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th>
|
||||
Description
|
||||
</th>
|
||||
<th>
|
||||
Count
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<th id="listing-showcase-id">
|
||||
-
|
||||
</th>
|
||||
<th id="listing-showcase-name">
|
||||
-
|
||||
</th>
|
||||
<th id="listing-showcase-description">
|
||||
-
|
||||
</th>
|
||||
<th id="listing-showcase-count">
|
||||
-
|
||||
</th>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{{/* .ShopID: default to path param "shopID" */}}
|
||||
|
||||
{{- $shopID := or (and .PathParams .PathParams.shopID) .ShopID }}
|
||||
|
||||
{{ $acctID := .Identity.Account.AccountID }}
|
||||
|
||||
<select
|
||||
class="min-w-fit cursor-pointer"
|
||||
_="
|
||||
on input
|
||||
set dataset to (my selectedOptions)[0].dataset
|
||||
send setListing(listing: dataset) to the closest <tr/>
|
||||
"
|
||||
>
|
||||
{{- $listings := .Accounts.GetListingsForShop $acctID $shopID }}
|
||||
{{- $dot := . }}
|
||||
<option disabled selected>- Listings -</option>
|
||||
{{- range $listing := $listings }}
|
||||
<option
|
||||
value="{{$listing.SKU}}"
|
||||
data-name="{{ $listing.Name }}"
|
||||
data-sku="{{ $listing.SKU }}"
|
||||
data-description="{{ $listing.Description }}"
|
||||
data-count="{{ $listing.Count }}"
|
||||
>
|
||||
{{ $listing.Name }}
|
||||
</option>
|
||||
{{- end }}
|
||||
</select>
|
||||
@@ -27,7 +27,7 @@
|
||||
{{- $acct := .dot.Identity.Account }}
|
||||
{{- $acctID := 0 }}
|
||||
{{- if $acct }}
|
||||
{{- $acctID = $acct.ID }}
|
||||
{{- $acctID = $acct.AccountID }}
|
||||
{{- end }}
|
||||
|
||||
{{- if not $userID }}
|
||||
|
||||
@@ -11,12 +11,13 @@
|
||||
|
||||
<script src="/scripts/htmx.min.js.gz"></script>
|
||||
<script src="/scripts/_hyperscript.min.js.gz"></script>
|
||||
<script src="https://unpkg.com/htmx-ext-path-params@2.0.0/path-params.js"></script>
|
||||
|
||||
{{/* This is where the page_head template will be inserted into the page */}}
|
||||
{{- block "head" . }}{{ end }}
|
||||
</head>
|
||||
|
||||
<body class="basis-full grow bg-background flex flex-col items-stretch">
|
||||
<body class="basis-full grow bg-background flex flex-col items-stretch" hx-ext="path-params">
|
||||
<header>
|
||||
<nav class="flex flex-col items-center text-xl overflow-x-scroll overflow-y-hidden ">
|
||||
{{- $path := or .Request.URL.Path "/" }}
|
||||
@@ -33,7 +34,7 @@
|
||||
{{- $acct := .Identity.Account }}
|
||||
{{- $acctID := 0 }}
|
||||
{{- if $acct }}
|
||||
{{- $acctID = $acct.ID }}
|
||||
{{- $acctID = $acct.AccountID }}
|
||||
{{- end }}
|
||||
|
||||
{{- if not $userID }}
|
||||
@@ -53,14 +54,14 @@
|
||||
</li>
|
||||
|
||||
<li class="pt-[1em] pb-[1em]">
|
||||
<a href="/accounts/{{$acctID}}/reports" class="p-[1em] font-bold {{ if eq $path (printf "/accounts/%d/reports" $acctID) }}selected{{ end }}">
|
||||
Reports
|
||||
<a href="/accounts/{{$acctID}}/inventory" class="p-[1em] font-bold {{ if eq $path (printf "/accounts/%d/inventory" $acctID) }}selected{{ end }}">
|
||||
Inventory
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="pt-[1em] pb-[1em]">
|
||||
<a href="/accounts/{{$acctID}}/inventory" class="p-[1em] font-bold {{ if eq $path (printf "/accounts/%d/inventory" $acctID) }}selected{{ end }}">
|
||||
Inventory
|
||||
<a href="/accounts/{{$acctID}}/reports" class="p-[1em] font-bold {{ if eq $path (printf "/accounts/%d/reports" $acctID) }}selected{{ end }}">
|
||||
Reports
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
|
||||
<h1>Account: {{ $acctID.Email }}</h1>
|
||||
|
||||
{{- $etsyUser := .Etsy.GetUserPointerByAccountID $acctID.ID }}
|
||||
{{- $etsyUser := .Etsy.GetUserPointerByAccountID $acctID.AccountID }}
|
||||
{{- if $etsyUser }}
|
||||
<h3>Etsy User: {{ $etsyUser.UserID }}; Shop ID: {{ $etsyUser.ShopID }}</h3>
|
||||
{{- else }}
|
||||
<h3>
|
||||
{{/* TODO: create this link dynamically, not EVERYTIME THE PAGE IS LOADED */}}
|
||||
<a href="{{ printf "/webhooks/etsy/%d/new-account-link" $acctID.ID }}">
|
||||
<a href="{{ printf "/webhooks/etsy/%d/new-account-link" $acctID.AccountID }}">
|
||||
Link Your Etsy Store!
|
||||
</a>
|
||||
</h3>
|
||||
{{- end }}
|
||||
<h2><a href="/accounts/{{$acctID.ID}}/reports">View Reports</a></h2>
|
||||
<h2><a href="/accounts/{{$acctID.AccountID}}/reports">View Reports</a></h2>
|
||||
|
||||
@@ -2,4 +2,38 @@
|
||||
|
||||
{{- define "title" }} Inventory++ {{ end }}
|
||||
|
||||
<h1>Inventory management page: WIP</h1>
|
||||
<h1 class="text-center mb-[0.5em]">Inventory</h1>
|
||||
|
||||
<h2 class="text-center mb-[0.5em]">Sync Stores</h2>
|
||||
|
||||
{{ $acctID := .Identity.Account.AccountID }}
|
||||
{{ $stores := .Accounts.GetShops $acctID }}
|
||||
|
||||
<section class="w-full flex flex-col items-center">
|
||||
<h3>
|
||||
Draft 2
|
||||
</h3>
|
||||
{{ componentBody "accounts/{acctID}/inventory/sync-table-v2" "dot" . }}
|
||||
</section>
|
||||
|
||||
<section class="w-full flex flex-col items-center">
|
||||
<h3>
|
||||
Draft 1
|
||||
</h3>
|
||||
{{ componentBody "accounts/{acctID}/inventory/sync-table" "dot" . }}
|
||||
</section>
|
||||
|
||||
<section class="flex justify-center mt-[1em]">
|
||||
{{ componentBody "accounts/{acctID}/inventory/sync-table/showcase-table" "dot" . }}
|
||||
</section>
|
||||
|
||||
<section class="w-full flex justify-center mt-[1em]">
|
||||
<button
|
||||
class="border-thin bg-card rounded-sm p-[0.5em] font-semibold cursor-pointer hover:underline hover:bg-accent"
|
||||
hx-get="/accounts/{{$acctID}}/inventory/sync-table/new-row"
|
||||
hx-target="#inventory-table > tbody"
|
||||
hx-swap="beforeend"
|
||||
>
|
||||
Add Row
|
||||
</button>
|
||||
</section>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
</section>
|
||||
|
||||
{{- if and .Identity.User.UserID (not .Identity.Account) }}
|
||||
{{/* TODO: make account creation automatic as part of the user creation process */}}
|
||||
<h2><a href="/account-creation">New Account</a></h2>
|
||||
{{- end }}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user