From a7c8fe4d64cfa3bd332313f7ba6e4cb462f5aef4 Mon Sep 17 00:00:00 2001 From: Angel Beltran Date: Fri, 9 Jan 2026 18:03:54 -0700 Subject: [PATCH] first drafts for inventory sync page --- diagrams/database_schema.svg | 2 +- diagrams/database_schema.uml | 1 + go.mod | 9 +- go.sum | 21 ++ internal/domains/accounts/accounts.go | 154 ++++++++--- internal/domains/accounts/dev.go | 243 ++++++++++++++++++ .../domains/accounts/store_with_context.go | 52 ++++ internal/domains/platforms/etsy/etsy.go | 15 +- .../platforms/etsy/platform_with_context.go | 63 +++++ internal/domains/platforms/etsy/store.go | 4 - internal/domains/raw_events/events.go | 22 +- .../domains/raw_events/store_with_context.go | 28 ++ internal/site/accounts.go | 2 +- internal/site/auth.go | 2 +- internal/site/login.go | 2 +- internal/site/response/body.go | 12 + internal/site/response/cookie.go | 12 + internal/site/response/html.go | 93 +++++++ internal/site/response/json.go | 8 + internal/site/response/redirect.go | 12 + internal/site/response/response.go | 2 + internal/site/response/status.go | 12 + internal/site/response/write.go | 6 +- internal/site/styles/borders.css | 22 +- internal/site/styles/components.css | 76 ++++++ internal/site/styles/index.css | 133 +++++++++- internal/site/styles/source.css | 1 + internal/site/styles/typography.css | 20 +- internal/site/templates.go | 46 ++-- .../inventory/sync-table-v2.html.tmpl | 65 +++++ .../inventory/sync-table-v2/new-row.html.tmpl | 64 +++++ .../{acctID}/inventory/sync-table.html.tmpl | 46 ++++ .../inventory/sync-table/new-row.html.tmpl | 54 ++++ .../sync-table/showcase-table.html.tmpl | 40 +++ .../shops/{shopID}/listing-select.html.tmpl | 29 +++ .../component_bodies/nav_bar.html.tmpl | 2 +- internal/site/templates/layout.html.tmpl | 13 +- .../accounts/{acctID}/index.html.tmpl | 6 +- .../accounts/{acctID}/inventory.html.tmpl | 36 ++- .../templates/page_bodies/index.html.tmpl | 1 + 40 files changed, 1310 insertions(+), 121 deletions(-) create mode 100644 internal/domains/accounts/dev.go create mode 100644 internal/domains/accounts/store_with_context.go create mode 100644 internal/domains/platforms/etsy/platform_with_context.go create mode 100644 internal/domains/raw_events/store_with_context.go create mode 100644 internal/site/response/html.go create mode 100644 internal/site/styles/components.css create mode 100644 internal/site/templates/component_bodies/accounts/{acctID}/inventory/sync-table-v2.html.tmpl create mode 100644 internal/site/templates/component_bodies/accounts/{acctID}/inventory/sync-table-v2/new-row.html.tmpl create mode 100644 internal/site/templates/component_bodies/accounts/{acctID}/inventory/sync-table.html.tmpl create mode 100644 internal/site/templates/component_bodies/accounts/{acctID}/inventory/sync-table/new-row.html.tmpl create mode 100644 internal/site/templates/component_bodies/accounts/{acctID}/inventory/sync-table/showcase-table.html.tmpl create mode 100644 internal/site/templates/component_bodies/accounts/{acctID}/platforms/{platform}/shops/{shopID}/listing-select.html.tmpl diff --git a/diagrams/database_schema.svg b/diagrams/database_schema.svg index 02a62f1..4f8d416 100644 --- a/diagrams/database_schema.svg +++ b/diagrams/database_schema.svg @@ -1 +1 @@ -accountsaccount_id:serial [PK]email:textverified:booleanuser_id:text [FK]etsy_access_tokensuser_id:integer [PK][FK]access_token:textrefresh_token:textaccess_token_expiration:timestamp with time zonerefresh_token_expiration:timestamp with time zoneetsy_oauth_requestsstate:bytea [PK]code_verifier:byteaexpiration:timestamp with time zoneaccount_id:integer [FK]etsy_store_eventsstore_id:text [PK][FK]event_timestamp:timestamp with time zone [PK][FK]event_id:text [PK][FK]platform:text [FK]etsy_usersuser_id:integer [PK]account_id:integer [FK]shop_id:integeroauth_login_statesstate:bytea [PK]expiration:timestamp with time zoneoauth_tokensaccess_token:text [PK]token_type:textrefresh_token:textexpiry:timestamp with time zoneid_token_issuer:textid_token_audience:text[]id_token_subject:text [FK]id_token_expiry:timestamp with time zoneid_token_issued_at:timestamp with time zoneid_token_nonce:textid_token_access_token_hash:textclaims:jsonboauth_usersuser_id:text [PK]raw_store_eventsplatform:text [PK]store_id:text [PK]event_timestamp:timestamp with time zone [PK]event_id:text [PK]raw_payload:jsonbschema_migrationsversion:bigint [PK]dirty:booleantiktok_store_eventsstore_id:text [PK][FK]event_timestamp:timestamp with time zone [PK][FK]event_id:text [PK][FK]platform:text [FK]wix_store_eventsstore_id:text [PK][FK]event_timestamp:timestamp with time zone [PK][FK]event_id:text [PK][FK]platform:text [FK] \ No newline at end of file +accountsaccount_id:serial [PK]email:textverified:booleanuser_id:text [FK]etsy_access_tokensuser_id:integer [PK][FK]access_token:textrefresh_token:textaccess_token_expiration:timestamp with time zonerefresh_token_expiration:timestamp with time zoneetsy_oauth_requestsstate:bytea [PK]code_verifier:byteaexpiration:timestamp with time zoneaccount_id:integer [FK]etsy_store_eventsstore_id:text [PK][FK]event_timestamp:timestamp with time zone [PK][FK]event_id:text [PK][FK]platform:text [FK]etsy_usersuser_id:integer [PK]account_id:integer [FK]shop_id:integeroauth_login_statesstate:bytea [PK]expiration:timestamp with time zonetarget_uri:textoauth_tokensaccess_token:text [PK]token_type:textrefresh_token:textexpiry:timestamp with time zoneid_token_issuer:textid_token_audience:text[]id_token_subject:text [FK]id_token_expiry:timestamp with time zoneid_token_issued_at:timestamp with time zoneid_token_nonce:textid_token_access_token_hash:textclaims:jsonboauth_usersuser_id:text [PK]raw_store_eventsplatform:text [PK]store_id:text [PK]event_timestamp:timestamp with time zone [PK]event_id:text [PK]raw_payload:jsonbschema_migrationsversion:bigint [PK]dirty:booleantiktok_store_eventsstore_id:text [PK][FK]event_timestamp:timestamp with time zone [PK][FK]event_id:text [PK][FK]platform:text [FK]wix_store_eventsstore_id:text [PK][FK]event_timestamp:timestamp with time zone [PK][FK]event_id:text [PK][FK]platform:text [FK] \ No newline at end of file diff --git a/diagrams/database_schema.uml b/diagrams/database_schema.uml index e171f87..881bf41 100644 --- a/diagrams/database_schema.uml +++ b/diagrams/database_schema.uml @@ -46,6 +46,7 @@ entity "**oauth_login_states**" { + ""state"": //bytea [PK]// -- *""expiration"": //timestamp with time zone // + *""target_uri"": //text // } entity "**oauth_tokens**" { diff --git a/go.mod b/go.mod index 2a7fda9..f7d70f2 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/alecthomas/kingpin v2.2.6+incompatible // indirect github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // indirect + github.com/angelbeltran/concurry v0.0.0-20260109231127-8e2678d8928c // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect @@ -26,6 +27,7 @@ require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect @@ -40,14 +42,16 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/speakeasy-api/jsonpath v0.6.0 // indirect github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.9 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect golang.org/x/crypto v0.46.0 // indirect - golang.org/x/mod v0.30.0 // indirect + golang.org/x/mod v0.31.0 // indirect golang.org/x/net v0.48.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/text v0.32.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/tools v0.40.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect @@ -56,5 +60,6 @@ require ( tool ( github.com/achiku/planter + github.com/angelbeltran/concurry github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen ) diff --git a/go.sum b/go.sum index 1569c19..c886b34 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,14 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 h1:ez/4by2iGztzR4L0zgAOR8lTQK9VlyBVVd7G4omaOQs= github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/angelbeltran/concurry v0.0.0-20260109213122-ef9f8bd8865f h1:9KBIsU6vHBWD/BGvxaFl45SNOE5aWHibZ3F5MdfFPsQ= +github.com/angelbeltran/concurry v0.0.0-20260109213122-ef9f8bd8865f/go.mod h1:Aii6F8lZCJb8Ns0d1FH35bDxNoyiuvwXd5BYJroJnks= +github.com/angelbeltran/concurry v0.0.0-20260109213220-20157174e154 h1:HFkK1uyOQ5nZgrQRyvebyLPMJGkouRAS26ljn8EsU0g= +github.com/angelbeltran/concurry v0.0.0-20260109213220-20157174e154/go.mod h1:Aii6F8lZCJb8Ns0d1FH35bDxNoyiuvwXd5BYJroJnks= +github.com/angelbeltran/concurry v0.0.0-20260109224857-024377724e7e h1:bsEp3zk309jiuKMwi5pS2rjolwvVLCfpSwUSNM/MZ04= +github.com/angelbeltran/concurry v0.0.0-20260109224857-024377724e7e/go.mod h1:Aii6F8lZCJb8Ns0d1FH35bDxNoyiuvwXd5BYJroJnks= +github.com/angelbeltran/concurry v0.0.0-20260109231127-8e2678d8928c h1:BIa061RHaIVP9TjpIQf0PldVpQmeln/2U1Cf2w8ALNI= +github.com/angelbeltran/concurry v0.0.0-20260109231127-8e2678d8928c/go.mod h1:Aii6F8lZCJb8Ns0d1FH35bDxNoyiuvwXd5BYJroJnks= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= @@ -15,6 +23,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/coreos/go-oidc/v3 v3.8.0 h1:s3e30r6VEl3/M7DTSCEuImmrfu1/1WBgA0cXkdzkrAY= github.com/coreos/go-oidc/v3 v3.8.0/go.mod h1:yQzSCqBnK3e6Fs5l+f5i0F8Kwf0zpH9bPEsbY00KanM= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -58,6 +67,8 @@ github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -113,12 +124,17 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8= github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -136,6 +152,7 @@ github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIj github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -147,6 +164,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -199,6 +218,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/domains/accounts/accounts.go b/internal/domains/accounts/accounts.go index 9438771..cc81d50 100644 --- a/internal/domains/accounts/accounts.go +++ b/internal/domains/accounts/accounts.go @@ -1,5 +1,8 @@ package accounts +// to generate StoreWithContext +//go:generate concurry -s Store + import ( "context" "errors" @@ -15,20 +18,51 @@ type ( db *pgxpool.Pool } - StoreWithContext struct { - ctx context.Context - db *Store - } - Account struct { - UserID string - ID int64 - Email string + AccountIDs + OAuthUser + Email string } OAuthUser struct { UserID string } + + AccountShop struct { + AccountShopIDs + Platform Platform + Name string + } + + Listing struct { + AccountIDs + Platform Platform + ShopID string + ListingID int64 + SKU string + Name string + Description string + Count int64 + } + + // ids + + AccountIDs struct { + AccountID int64 + } + + AccountShopIDs struct { + AccountIDs + ShopID string + } + + Platform string +) + +const ( + Etsy Platform = "Etsy" + Tiktok Platform = "Tiktok" + Wix Platform = "Wix" ) func NewStore(db *pgxpool.Pool) *Store { @@ -38,10 +72,7 @@ func NewStore(db *pgxpool.Pool) *Store { } func (db *Store) WithContext(ctx context.Context) *StoreWithContext { - return &StoreWithContext{ - ctx: ctx, - db: db, - } + return NewStoreWithContext(ctx, db) } func (db *Store) CreateAccount(ctx context.Context, userID, email string) (Account, error) { @@ -78,16 +109,20 @@ func (db *Store) CreateAccount(ctx context.Context, userID, email string) (Accou } return Account{ - UserID: userID, - ID: acctID, - Email: email, + OAuthUser: OAuthUser{ + UserID: userID, + }, + AccountIDs: AccountIDs{ + AccountID: acctID, + }, + Email: email, }, nil } func (db *Store) GetAccount(ctx context.Context, id int64) (Account, error) { rows, err := db.db.Query( ctx, - "SELECT email FROM accounts WHERE account_id = @account_id", + "SELECT email, user_id FROM accounts WHERE account_id = @account_id", pgx.NamedArgs{ "account_id": id, }, @@ -96,7 +131,12 @@ func (db *Store) GetAccount(ctx context.Context, id int64) (Account, error) { return Account{}, fmt.Errorf("failed to perform query: %w", err) } - email, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) + type Row struct { + Email string + User_id string + } + + r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Row]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return Account{}, consts.ErrNotFound @@ -106,8 +146,13 @@ func (db *Store) GetAccount(ctx context.Context, id int64) (Account, error) { } return Account{ - ID: id, - Email: email, + AccountIDs: AccountIDs{ + AccountID: id, + }, + OAuthUser: OAuthUser{ + r.User_id, + }, + Email: r.Email, }, nil } @@ -143,16 +188,20 @@ func (db *Store) GetAccountByUserID(ctx context.Context, userID string) (Account } return Account{ - UserID: userID, - ID: r.Account_ID, - Email: r.Email, + AccountIDs: AccountIDs{ + AccountID: r.Account_ID, + }, + OAuthUser: OAuthUser{ + UserID: userID, + }, + Email: r.Email, }, nil } func (db *Store) GetAccountByEmail(ctx context.Context, email string) (Account, error) { rows, err := db.db.Query( ctx, - "SELECT account_id FROM accounts WHERE email = @email", + "SELECT account_id, user_id FROM accounts WHERE email = @email", pgx.NamedArgs{ "email": email, }, @@ -161,7 +210,12 @@ func (db *Store) GetAccountByEmail(ctx context.Context, email string) (Account, return Account{}, fmt.Errorf("failed to perform query: %w", err) } - id, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[int64]) + type Row struct { + Account_id int64 + User_id string + } + + r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Row]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return Account{}, consts.ErrNotFound @@ -171,7 +225,12 @@ func (db *Store) GetAccountByEmail(ctx context.Context, email string) (Account, } return Account{ - ID: id, + AccountIDs: AccountIDs{ + AccountID: r.Account_id, + }, + OAuthUser: OAuthUser{ + UserID: r.User_id, + }, Email: email, }, nil } @@ -213,28 +272,53 @@ func (db *Store) GetUserAndAccountByAccessToken(ctx context.Context, accessToken return OAuthUser{}, nil, fmt.Errorf("failed to scan row: %w", err) } + user := OAuthUser{UserID: r.User_ID} + var acct *Account if r.Account_ID != nil { acct = &Account{ - UserID: r.User_ID, - ID: *r.Account_ID, - Email: *r.Email, + AccountIDs: AccountIDs{ + AccountID: *r.Account_ID, + }, + OAuthUser: user, + Email: *r.Email, } } - return OAuthUser{UserID: r.User_ID}, acct, nil + return user, acct, nil } -func (db *StoreWithContext) CreateAccount(userID, email string) (Account, error) { - return db.db.CreateAccount(db.ctx, userID, email) +func (db *Store) GetShops(ctx context.Context, acctID int64) ([]AccountShop, error) { + var shops []AccountShop + for _, v := range devShops { + if v.AccountID != acctID { + continue + } + shops = append(shops, v) + } + + return shops, nil } -func (db *StoreWithContext) GetAccount(id int64) (Account, error) { - return db.db.GetAccount(db.ctx, id) +func (db *Store) GetListingsForShop(ctx context.Context, acctID int64, shopID string) ([]Listing, error) { + var vs []Listing + for _, v := range devListings { + if v.AccountID != acctID { + continue + } + if v.ShopID != shopID { + continue + } + vs = append(vs, v) + } + + return vs, nil } -func (db *StoreWithContext) GetAccountPointerByUserID(userID string) (*Account, error) { - acct, err := db.db.GetAccountByUserID(db.ctx, userID) +// additional context + +func (db *Store) GetAccountPointerByUserID(ctx context.Context, userID string) (*Account, error) { + acct, err := db.GetAccountByUserID(ctx, userID) if err == nil { return &acct, nil } diff --git a/internal/domains/accounts/dev.go b/internal/domains/accounts/dev.go new file mode 100644 index 0000000..8c1f7e2 --- /dev/null +++ b/internal/domains/accounts/dev.go @@ -0,0 +1,243 @@ +package accounts + +var ( + devShops = []AccountShop{ + AccountShop{ + AccountShopIDs: AccountShopIDs{ + AccountIDs: AccountIDs{ + AccountID: 6, + }, + ShopID: "2", + }, + Platform: Etsy, + Name: "Etsy 1", + }, + AccountShop{ + AccountShopIDs: AccountShopIDs{ + AccountIDs: AccountIDs{ + AccountID: 6, + }, + ShopID: "5", + }, + Platform: Etsy, + Name: "Etsy 2", + }, + AccountShop{ + AccountShopIDs: AccountShopIDs{ + AccountIDs: AccountIDs{ + AccountID: 6, + }, + ShopID: "8", + }, + Platform: Etsy, + Name: "Etsy 3", + }, + AccountShop{ + AccountShopIDs: AccountShopIDs{ + AccountIDs: AccountIDs{ + AccountID: 6, + }, + ShopID: "11", + }, + Platform: Tiktok, + Name: "Tiktok 1", + }, + AccountShop{ + AccountShopIDs: AccountShopIDs{ + AccountIDs: AccountIDs{ + AccountID: 6, + }, + ShopID: "14", + }, + Platform: Tiktok, + Name: "Tiktok 2", + }, + AccountShop{ + AccountShopIDs: AccountShopIDs{ + AccountIDs: AccountIDs{ + AccountID: 6, + }, + ShopID: "17", + }, + Platform: Tiktok, + Name: "Tiktok 3", + }, + AccountShop{ + AccountShopIDs: AccountShopIDs{ + AccountIDs: AccountIDs{ + AccountID: 6, + }, + ShopID: "20", + }, + Platform: Wix, + Name: "Wix 1", + }, + AccountShop{ + AccountShopIDs: AccountShopIDs{ + AccountIDs: AccountIDs{ + AccountID: 6, + }, + ShopID: "23", + }, + Platform: Wix, + Name: "Wix 2", + }, + AccountShop{ + AccountShopIDs: AccountShopIDs{ + AccountIDs: AccountIDs{ + AccountID: 6, + }, + ShopID: "26", + }, + Platform: Wix, + Name: "Wix 3", + }, + } + + devListings = []Listing{ + { + AccountIDs: AccountIDs{ + AccountID: 6, + }, + Platform: Etsy, + ShopID: "1", + ListingID: 1, + SKU: "sku 1", + Name: "name 1", + Description: "description 1", + Count: 51, + }, + { + AccountIDs: AccountIDs{ + AccountID: 6, + }, + Platform: Etsy, + ShopID: "1", + ListingID: 2, + SKU: "sku 2", + Name: "name 2", + Description: "description 2", + Count: 52, + }, + { + AccountIDs: AccountIDs{ + AccountID: 6, + }, + Platform: Etsy, + ShopID: "1", + ListingID: 3, + SKU: "sku 3", + Name: "name 3", + Description: "description 3", + Count: 53, + }, + { + AccountIDs: AccountIDs{ + AccountID: 6, + }, + Platform: Tiktok, + ShopID: "4", + ListingID: 4, + SKU: "sku 4", + Name: "name 4", + Description: "description 4", + Count: 54, + }, + { + AccountIDs: AccountIDs{ + AccountID: 6, + }, + Platform: Tiktok, + ShopID: "4", + ListingID: 5, + SKU: "sku 5", + Name: "name 5", + Description: "description 5", + Count: 55, + }, + { + AccountIDs: AccountIDs{ + AccountID: 6, + }, + Platform: Tiktok, + ShopID: "4", + ListingID: 6, + SKU: "sku 6", + Name: "name 6", + Description: "description 6", + Count: 56, + }, + { + AccountIDs: AccountIDs{ + AccountID: 6, + }, + Platform: Wix, + ShopID: "7", + ListingID: 7, + SKU: "sku 7", + Name: "name 7", + Description: "description 7", + Count: 57, + }, + { + AccountIDs: AccountIDs{ + AccountID: 6, + }, + Platform: Wix, + ShopID: "7", + ListingID: 8, + SKU: "sku 8", + Name: "name 8", + Description: "description 8", + Count: 58, + }, + { + AccountIDs: AccountIDs{ + AccountID: 6, + }, + Platform: Wix, + ShopID: "7", + ListingID: 9, + SKU: "sku 9", + Name: "name 9", + Description: "description 9", + Count: 59, + }, + { + AccountIDs: AccountIDs{ + AccountID: 6, + }, + Platform: Etsy, + ShopID: "2", + ListingID: 10, + SKU: "sku 10", + Name: "name 10", + Description: "description 10", + Count: 60, + }, + { + AccountIDs: AccountIDs{ + AccountID: 6, + }, + Platform: Etsy, + ShopID: "2", + ListingID: 11, + SKU: "sku 11", + Name: "name 11", + Description: "description 11", + Count: 61, + }, + { + AccountIDs: AccountIDs{ + AccountID: 6, + }, + Platform: Etsy, + ShopID: "2", + ListingID: 12, + SKU: "sku 12", + Name: "name 12", + Description: "description 12", + Count: 62, + }, + } +) diff --git a/internal/domains/accounts/store_with_context.go b/internal/domains/accounts/store_with_context.go new file mode 100644 index 0000000..73c8b44 --- /dev/null +++ b/internal/domains/accounts/store_with_context.go @@ -0,0 +1,52 @@ +// Code generated by concurry DO NOT EDIT. +// https://github.com/angelbeltran/concurry +// concurry +package accounts + +import ( + "context" +) + +type StoreWithContext struct { + ctx context.Context + *Store +} + +func NewStoreWithContext(ctx context.Context, v *Store) *StoreWithContext { + return &StoreWithContext{ + ctx: ctx, + Store: v, + } +} + +func (v_ctx *StoreWithContext) CreateAccount(userID string, email string) (Account, error) { + return v_ctx.Store.CreateAccount(v_ctx.ctx, userID, email) +} + +func (v_ctx *StoreWithContext) GetAccount(id int64) (Account, error) { + return v_ctx.Store.GetAccount(v_ctx.ctx, id) +} + +func (v_ctx *StoreWithContext) GetAccountByUserID(userID string) (Account, error) { + return v_ctx.Store.GetAccountByUserID(v_ctx.ctx, userID) +} + +func (v_ctx *StoreWithContext) GetAccountByEmail(email string) (Account, error) { + return v_ctx.Store.GetAccountByEmail(v_ctx.ctx, email) +} + +func (v_ctx *StoreWithContext) GetUserAndAccountByAccessToken(accessToken string) (OAuthUser, *Account, error) { + return v_ctx.Store.GetUserAndAccountByAccessToken(v_ctx.ctx, accessToken) +} + +func (v_ctx *StoreWithContext) GetShops(acctID int64) ([]AccountShop, error) { + return v_ctx.Store.GetShops(v_ctx.ctx, acctID) +} + +func (v_ctx *StoreWithContext) GetListingsForShop(acctID int64, shopID string) ([]Listing, error) { + return v_ctx.Store.GetListingsForShop(v_ctx.ctx, acctID, shopID) +} + +func (v_ctx *StoreWithContext) GetAccountPointerByUserID(userID string) (*Account, error) { + return v_ctx.Store.GetAccountPointerByUserID(v_ctx.ctx, userID) +} diff --git a/internal/domains/platforms/etsy/etsy.go b/internal/domains/platforms/etsy/etsy.go index fddcbcf..ab82a54 100644 --- a/internal/domains/platforms/etsy/etsy.go +++ b/internal/domains/platforms/etsy/etsy.go @@ -20,6 +20,7 @@ import ( ) //go:generate oapi-codegen -generate types,client -package generated_client -o generated_client/client.go openapi.3.0.2.json +//go:generate concurry -s Platform // TODO: // - [x] document the flow in the README.md @@ -36,11 +37,6 @@ type ( apiSharedSecret string db *pgxpool.Pool } - - PlatformWithContext struct { - ctx context.Context - p *Platform - } ) // oauth scopes @@ -77,10 +73,7 @@ func NewPlatform(oAuthRedirectURI func(acctID int64) string, apiKeystring, apiSh } func (p *Platform) WithContext(ctx context.Context) *PlatformWithContext { - return &PlatformWithContext{ - ctx: ctx, - p: p, - } + return NewPlatformWithContext(ctx, p) } func (p *Platform) GenerateConnectionURLForNewAccount(ctx context.Context, acctID int64) (*url.URL, error) { @@ -113,10 +106,6 @@ func (p *Platform) GenerateConnectionURLForNewAccount(ctx context.Context, acctI }, nil } -func (p *PlatformWithContext) GenerateConnectionURLForNewAccount(acctID int64) (*url.URL, error) { - return p.p.GenerateConnectionURLForNewAccount(p.ctx, acctID) -} - // HandleNewAuthCode handles the auth code to get api access func (p *Platform) HandleNewAuthCode(ctx context.Context, acctID int64, state, authCode string) (bool, error) { // look up matching oauth request diff --git a/internal/domains/platforms/etsy/platform_with_context.go b/internal/domains/platforms/etsy/platform_with_context.go new file mode 100644 index 0000000..1c50f11 --- /dev/null +++ b/internal/domains/platforms/etsy/platform_with_context.go @@ -0,0 +1,63 @@ +// Code generated by concurry DO NOT EDIT. +// https://github.com/angelbeltran/concurry +// concurry +package etsy + +import ( + "context" + "github.com/google/uuid" + "net/url" + "time" +) + +type PlatformWithContext struct { + ctx context.Context + *Platform +} + +func NewPlatformWithContext(ctx context.Context, v *Platform) *PlatformWithContext { + return &PlatformWithContext{ + ctx: ctx, + Platform: v, + } +} + +func (v_ctx *PlatformWithContext) GenerateConnectionURLForNewAccount(acctID int64) (*url.URL, error) { + return v_ctx.Platform.GenerateConnectionURLForNewAccount(v_ctx.ctx, acctID) +} + +func (v_ctx *PlatformWithContext) HandleNewAuthCode(acctID int64, state string, authCode string) (bool, error) { + return v_ctx.Platform.HandleNewAuthCode(v_ctx.ctx, acctID, state, authCode) +} + +func (v_ctx *PlatformWithContext) parseAccessCodeResponseBody(body []byte) (string, string, time.Time, int64, error) { + return v_ctx.Platform.parseAccessCodeResponseBody(v_ctx.ctx, body) +} + +func (v_ctx *PlatformWithContext) getNewUserShopID(accessToken string, userID int64) (int64, error) { + return v_ctx.Platform.getNewUserShopID(v_ctx.ctx, accessToken, userID) +} + +func (v_ctx *PlatformWithContext) GetUserPointerByAccountID(acctID int64) (*EtsyUser, error) { + return v_ctx.Platform.GetUserPointerByAccountID(v_ctx.ctx, acctID) +} + +func (v_ctx *PlatformWithContext) saveNewEtsyUser(user EtsyUser, tokens etsyAccessTokens) error { + return v_ctx.Platform.saveNewEtsyUser(v_ctx.ctx, user, tokens) +} + +func (v_ctx *PlatformWithContext) createNewOAuthRequest(acctID int64) (oauth2Request, error) { + return v_ctx.Platform.createNewOAuthRequest(v_ctx.ctx, acctID) +} + +func (v_ctx *PlatformWithContext) getOauthRequest(state uuid.UUID) (oauth2Request, bool, error) { + return v_ctx.Platform.getOauthRequest(v_ctx.ctx, state) +} + +func (v_ctx *PlatformWithContext) InvalidateState(state string) error { + return v_ctx.Platform.InvalidateState(v_ctx.ctx, state) +} + +func (v_ctx *PlatformWithContext) deleteOauthRequest(state uuid.UUID) error { + return v_ctx.Platform.deleteOauthRequest(v_ctx.ctx, state) +} diff --git a/internal/domains/platforms/etsy/store.go b/internal/domains/platforms/etsy/store.go index 2a684fe..afcd74e 100644 --- a/internal/domains/platforms/etsy/store.go +++ b/internal/domains/platforms/etsy/store.go @@ -73,10 +73,6 @@ func (p *Platform) GetUserPointerByAccountID(ctx context.Context, acctID int64) }, nil } -func (p *PlatformWithContext) GetUserPointerByAccountID(acctID int64) (*EtsyUser, error) { - return p.p.GetUserPointerByAccountID(p.ctx, acctID) -} - func (p *Platform) saveNewEtsyUser( ctx context.Context, user EtsyUser, diff --git a/internal/domains/raw_events/events.go b/internal/domains/raw_events/events.go index e88ed1a..6273e40 100644 --- a/internal/domains/raw_events/events.go +++ b/internal/domains/raw_events/events.go @@ -16,12 +16,6 @@ type ( db *pgxpool.Pool } - // StoreWithContext allows currying all Store methods by context.Context - StoreWithContext struct { - ctx context.Context - db *Store - } - Event struct { Platform string StoreID string @@ -38,10 +32,7 @@ func NewStore(db *pgxpool.Pool) *Store { } func (db *Store) WithContext(ctx context.Context) *StoreWithContext { - return &StoreWithContext{ - ctx: ctx, - db: db, - } + return NewStoreWithContext(ctx, db) } func (db *Store) Save(ctx context.Context, e *Event) error { @@ -81,10 +72,6 @@ func (db *Store) Save(ctx context.Context, e *Event) error { return nil } -func (db *StoreWithContext) Save(e *Event) error { - return db.db.Save(db.ctx, e) -} - func (db *Store) LoadEventsForStore( ctx context.Context, platform string, @@ -125,10 +112,3 @@ func (db *Store) LoadEventsForStore( return evts, nil } - -func (db *StoreWithContext) LoadEventsForStore( - platform string, - storeID string, -) ([]Event, error) { - return db.db.LoadEventsForStore(db.ctx, platform, storeID) -} diff --git a/internal/domains/raw_events/store_with_context.go b/internal/domains/raw_events/store_with_context.go new file mode 100644 index 0000000..65e8799 --- /dev/null +++ b/internal/domains/raw_events/store_with_context.go @@ -0,0 +1,28 @@ +// Code generated by concurry DO NOT EDIT. +// https://github.com/angelbeltran/concurry +// concurry +package raw_events + +import ( + "context" +) + +type StoreWithContext struct { + ctx context.Context + *Store +} + +func NewStoreWithContext(ctx context.Context, v *Store) *StoreWithContext { + return &StoreWithContext{ + ctx: ctx, + Store: v, + } +} + +func (v_ctx *StoreWithContext) Save(e *Event) error { + return v_ctx.Store.Save(v_ctx.ctx, e) +} + +func (v_ctx *StoreWithContext) LoadEventsForStore(platform string, storeID string) ([]Event, error) { + return v_ctx.Store.LoadEventsForStore(v_ctx.ctx, platform, storeID) +} diff --git a/internal/site/accounts.go b/internal/site/accounts.go index c3bbddb..9fdf512 100644 --- a/internal/site/accounts.go +++ b/internal/site/accounts.go @@ -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 } diff --git a/internal/site/auth.go b/internal/site/auth.go index f82cb30..8b381d5 100644 --- a/internal/site/auth.go +++ b/internal/site/auth.go @@ -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) } diff --git a/internal/site/login.go b/internal/site/login.go index bc8e299..7b76751 100644 --- a/internal/site/login.go +++ b/internal/site/login.go @@ -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 diff --git a/internal/site/response/body.go b/internal/site/response/body.go index 2909578..0a877cc 100644 --- a/internal/site/response/body.go +++ b/internal/site/response/body.go @@ -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 +} diff --git a/internal/site/response/cookie.go b/internal/site/response/cookie.go index 2c9acab..3fd3468 100644 --- a/internal/site/response/cookie.go +++ b/internal/site/response/cookie.go @@ -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 +} diff --git a/internal/site/response/html.go b/internal/site/response/html.go new file mode 100644 index 0000000..258fdd2 --- /dev/null +++ b/internal/site/response/html.go @@ -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 +} diff --git a/internal/site/response/json.go b/internal/site/response/json.go index c3f1856..c4ed75f 100644 --- a/internal/site/response/json.go +++ b/internal/site/response/json.go @@ -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 +} diff --git a/internal/site/response/redirect.go b/internal/site/response/redirect.go index 17cd5b1..15dcb33 100644 --- a/internal/site/response/redirect.go +++ b/internal/site/response/redirect.go @@ -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 +} diff --git a/internal/site/response/response.go b/internal/site/response/response.go index 94394e6..da0ca57 100644 --- a/internal/site/response/response.go +++ b/internal/site/response/response.go @@ -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 } diff --git a/internal/site/response/status.go b/internal/site/response/status.go index 61a0929..9a722a5 100644 --- a/internal/site/response/status.go +++ b/internal/site/response/status.go @@ -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 +} diff --git a/internal/site/response/write.go b/internal/site/response/write.go index 43cb57c..398203f 100644 --- a/internal/site/response/write.go +++ b/internal/site/response/write.go @@ -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 { diff --git a/internal/site/styles/borders.css b/internal/site/styles/borders.css index c3dc870..7cb7998 100644 --- a/internal/site/styles/borders.css +++ b/internal/site/styles/borders.css @@ -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); } } diff --git a/internal/site/styles/components.css b/internal/site/styles/components.css new file mode 100644 index 0000000..582bb58 --- /dev/null +++ b/internal/site/styles/components.css @@ -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; + } + } + } +} diff --git a/internal/site/styles/index.css b/internal/site/styles/index.css index b6d9a69..0316e7b 100644 --- a/internal/site/styles/index.css +++ b/internal/site/styles/index.css @@ -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; diff --git a/internal/site/styles/source.css b/internal/site/styles/source.css index 73d911e..47b5bba 100644 --- a/internal/site/styles/source.css +++ b/internal/site/styles/source.css @@ -14,3 +14,4 @@ @import "./interactivity.css"; @import "./svg.css"; @import "./accessibility.css"; +@import "./components.css"; diff --git a/internal/site/styles/typography.css b/internal/site/styles/typography.css index a77c4ff..02f2c9c 100644 --- a/internal/site/styles/typography.css +++ b/internal/site/styles/typography.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 { diff --git a/internal/site/templates.go b/internal/site/templates.go index 284748b..1dada51 100644 --- a/internal/site/templates.go +++ b/internal/site/templates.go @@ -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) diff --git a/internal/site/templates/component_bodies/accounts/{acctID}/inventory/sync-table-v2.html.tmpl b/internal/site/templates/component_bodies/accounts/{acctID}/inventory/sync-table-v2.html.tmpl new file mode 100644 index 0000000..c28ffb8 --- /dev/null +++ b/internal/site/templates/component_bodies/accounts/{acctID}/inventory/sync-table-v2.html.tmpl @@ -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 -}} + + + + + + + + + + + + + + {{/* TODO: list existing row */}} + {{- componentBody "accounts/{acctID}/inventory/sync-table-v2/new-row" "dot" $dot }} + {{/* TODO: save the saved rows */}} + + + + + + + + + + + +
+ Shop + + Listing + + SKU + + Description + + +
+ Shop + + Listing + + SKU + + Description + + +
+ diff --git a/internal/site/templates/component_bodies/accounts/{acctID}/inventory/sync-table-v2/new-row.html.tmpl b/internal/site/templates/component_bodies/accounts/{acctID}/inventory/sync-table-v2/new-row.html.tmpl new file mode 100644 index 0000000..4890320 --- /dev/null +++ b/internal/site/templates/component_bodies/accounts/{acctID}/inventory/sync-table-v2/new-row.html.tmpl @@ -0,0 +1,64 @@ +{{/* .Identity.Account.AccountID, .Accounts */}} + +{{- $dot := or .dot . -}} + + +{{- $acctID := $dot.Identity.Account.AccountID }} +{{- $shops := $dot.Accounts.GetShops $acctID -}} + + + + + + + + - + + + - + + + - + + + + + diff --git a/internal/site/templates/component_bodies/accounts/{acctID}/inventory/sync-table.html.tmpl b/internal/site/templates/component_bodies/accounts/{acctID}/inventory/sync-table.html.tmpl new file mode 100644 index 0000000..88a06ae --- /dev/null +++ b/internal/site/templates/component_bodies/accounts/{acctID}/inventory/sync-table.html.tmpl @@ -0,0 +1,46 @@ +{{- $dot := or .dot . }} + +{{- $dot.Auth.ByMatchingAccountID 2 }} + +{{- $acctID := $dot.Identity.Account.AccountID }} +{{- $stores := $dot.Accounts.GetShops $acctID -}} + + + + + + + {{- range $store := $stores }} + + + {{- end }} + + + + + + {{- componentBody "accounts/{acctID}/inventory/sync-table/new-row" "dot" $dot }} + + + + + {{- range $store := $stores }} + + + {{- end }} + + + + +
+ {{ $store.Platform }}: {{ $store.Name }} + + Count + +
+ {{ $store.Platform }}: {{ $store.Name }} + + Count + +
+ diff --git a/internal/site/templates/component_bodies/accounts/{acctID}/inventory/sync-table/new-row.html.tmpl b/internal/site/templates/component_bodies/accounts/{acctID}/inventory/sync-table/new-row.html.tmpl new file mode 100644 index 0000000..a0a02bf --- /dev/null +++ b/internal/site/templates/component_bodies/accounts/{acctID}/inventory/sync-table/new-row.html.tmpl @@ -0,0 +1,54 @@ +{{- $dot := or .dot .}} + +{{- $dot.Auth.ByMatchingAccountID 2 }} + +{{- $acctID := $dot.Identity.Account.AccountID }} +{{- $shops := $dot.Accounts.GetShops $acctID -}} + + + +{{- range $shop := $shops }} + {{- $listings := $dot.Accounts.GetListingsForShop $acctID $shop.ShopID }} + + + + + +{{- end }} + + + + diff --git a/internal/site/templates/component_bodies/accounts/{acctID}/inventory/sync-table/showcase-table.html.tmpl b/internal/site/templates/component_bodies/accounts/{acctID}/inventory/sync-table/showcase-table.html.tmpl new file mode 100644 index 0000000..5be172a --- /dev/null +++ b/internal/site/templates/component_bodies/accounts/{acctID}/inventory/sync-table/showcase-table.html.tmpl @@ -0,0 +1,40 @@ + + + + + + + + + + + + + +
+ ID + + Name + + Description + + Count +
+ - + + - + + - + + - +
+ diff --git a/internal/site/templates/component_bodies/accounts/{acctID}/platforms/{platform}/shops/{shopID}/listing-select.html.tmpl b/internal/site/templates/component_bodies/accounts/{acctID}/platforms/{platform}/shops/{shopID}/listing-select.html.tmpl new file mode 100644 index 0000000..4b86710 --- /dev/null +++ b/internal/site/templates/component_bodies/accounts/{acctID}/platforms/{platform}/shops/{shopID}/listing-select.html.tmpl @@ -0,0 +1,29 @@ +{{/* .ShopID: default to path param "shopID" */}} + +{{- $shopID := or (and .PathParams .PathParams.shopID) .ShopID }} + +{{ $acctID := .Identity.Account.AccountID }} + + diff --git a/internal/site/templates/component_bodies/nav_bar.html.tmpl b/internal/site/templates/component_bodies/nav_bar.html.tmpl index ed1f481..28ae7c1 100644 --- a/internal/site/templates/component_bodies/nav_bar.html.tmpl +++ b/internal/site/templates/component_bodies/nav_bar.html.tmpl @@ -27,7 +27,7 @@ {{- $acct := .dot.Identity.Account }} {{- $acctID := 0 }} {{- if $acct }} - {{- $acctID = $acct.ID }} + {{- $acctID = $acct.AccountID }} {{- end }} {{- if not $userID }} diff --git a/internal/site/templates/layout.html.tmpl b/internal/site/templates/layout.html.tmpl index 8feb211..2a9fc3d 100644 --- a/internal/site/templates/layout.html.tmpl +++ b/internal/site/templates/layout.html.tmpl @@ -11,12 +11,13 @@ + {{/* This is where the page_head template will be inserted into the page */}} {{- block "head" . }}{{ end }} - +