diff --git a/datafetcher/functions/functions.go b/datafetcher/functions/functions.go index c9fcb69..aaf2377 100644 --- a/datafetcher/functions/functions.go +++ b/datafetcher/functions/functions.go @@ -1,6 +1,78 @@ package functions +import ( + "fmt" + "log" + "net/url" + "os" + "strconv" + "time" +) + const ( AccountIdQueryParam = "accountId" TransactionNumQueryParam = "numTransactions" + StartDateParam = "startDate" + EndDateParam = "endDate" ) + +// QueryParams struct to hold the query parameters +type QueryParams struct { + AccountID *string + NumTransactions *int32 + StartDate *time.Time + EndDate *time.Time +} + +func (q *QueryParams) String() string { + return fmt.Sprintf("QueryParams{AccountID: %s, NumTransactions: %d, StartDate: %s, EndDate: %s}", *q.AccountID, *q.NumTransactions, *q.StartDate, *q.EndDate) +} + +// FetchQueryParams extracts query parameters from the url.Values map +func FetchQueryParams(queryParams url.Values) *QueryParams { + params := &QueryParams{} + defaultTransactions := int32(20) + params.NumTransactions = &defaultTransactions + logger := log.New(os.Stdout, "query params:", log.Default().Flags()) + + // Fetch "accountId" + if accountID := queryParams.Get(AccountIdQueryParam); accountID != "" { + params.AccountID = &accountID + } + + // Fetch "numTransactions" + if numTransactions := queryParams.Get(TransactionNumQueryParam); numTransactions != "" { + if num, err := strconv.Atoi(numTransactions); err == nil { + myInt := int32(num) + params.NumTransactions = &myInt + } + } + + // Fetch "startDate" + if startDate := queryParams.Get(StartDateParam); startDate != "" { + log.Println(fmt.Sprintf("Start date is %s", startDate)) + timeStartDate, err := time.Parse(time.RFC3339, startDate) + if err == nil { + params.StartDate = &timeStartDate + } + log.Println(fmt.Sprintf("Parsed date is %s", timeStartDate)) + log.Println(fmt.Sprintf("Error in parsing date is %s", err)) + } + + // Fetch "endDate" + if endDate := queryParams.Get(EndDateParam); endDate != "" { + timeEndDate, err := time.Parse(time.RFC3339, endDate) + if err == nil { + params.EndDate = &timeEndDate + } + } + + if params.StartDate != nil && params.EndDate != nil { + maxTransactions := int32(10000) + params.NumTransactions = &maxTransactions + } + + logger.Println(fmt.Sprintf("query params are: %v", params)) + + return params +} diff --git a/datafetcher/handlers/transactions-csv.go b/datafetcher/handlers/transactions-csv.go index 1f16384..b3eddc3 100644 --- a/datafetcher/handlers/transactions-csv.go +++ b/datafetcher/handlers/transactions-csv.go @@ -7,14 +7,16 @@ import ( "log" "net/http" + "github.com/esteanes/expense-manager/datafetcher/functions" "github.com/esteanes/expense-manager/datafetcher/upclient" ) type TransactionsCsvHandler struct { *BaseHandler + *TransactionsHandler } -func NewTransactionCsvHandler(log *log.Logger, upclient *upclient.APIClient, auth context.Context) *TransactionsCsvHandler { +func NewTransactionCsvHandler(log *log.Logger, upclient *upclient.APIClient, auth context.Context, transactionsHandler *TransactionsHandler) *TransactionsCsvHandler { handler := &TransactionsCsvHandler{} handler.BaseHandler = &BaseHandler{ Uri: "/transactions-csv", @@ -22,40 +24,46 @@ func NewTransactionCsvHandler(log *log.Logger, upclient *upclient.APIClient, aut UpClient: upclient, UpAuth: auth, Handler: handler} + handler.TransactionsHandler = transactionsHandler return handler } func (h *TransactionsCsvHandler) Post(w http.ResponseWriter, r *http.Request) {} func (h *TransactionsCsvHandler) Get(w http.ResponseWriter, r *http.Request) { + queryParams := functions.FetchQueryParams(r.URL.Query()) w.Header().Set("Content-Type", "text/csv") w.Header().Set("Content-Disposition", "attachment;filename=transactions.csv") csvWriter := csv.NewWriter(w) - pageSize := int32(30) // int32 | The number of records to return in each page. (optional) - resp2, _, _ := h.UpClient.TransactionsAPI.TransactionsGet(h.UpAuth).PageSize(pageSize).Execute() + transactionsChannel := make(chan upclient.TransactionResource, *queryParams.NumTransactions) + if queryParams.AccountID == nil { + go h.getTransactionsForAllAccounts(transactionsChannel, queryParams) + } else { + go h.getTransactionsForSpecifiedAccount(transactionsChannel, queryParams) - header := []string{"Transaction Description", "Amount", "Date"} + header := []string{"Transaction Description", "Amount", "Date"} - h.Log.Printf("trying to generate a CSV") - // Print the JSON-formatted response - if err := csvWriter.Write(header); err != nil { - http.Error(w, "Error writing CSV header", http.StatusInternalServerError) - return - } - for _, transaction := range resp2.Data { - record := []string{ - transaction.Attributes.Description, - transaction.Attributes.Amount.Value, - transaction.Attributes.CreatedAt.String(), - } - if err := csvWriter.Write(record); err != nil { - fmt.Fprintf(w, "Error writing CSV line %v\n", err) + h.Log.Printf("trying to generate a CSV") + // Print the JSON-formatted response + if err := csvWriter.Write(header); err != nil { + http.Error(w, "Error writing CSV header", http.StatusInternalServerError) return } - } + for transaction := range transactionsChannel { + record := []string{ + transaction.Attributes.Description, + transaction.Attributes.Amount.Value, + transaction.Attributes.CreatedAt.String(), + } + if err := csvWriter.Write(record); err != nil { + fmt.Fprintf(w, "Error writing CSV line %v\n", err) + return + } + } - csvWriter.Flush() + csvWriter.Flush() - if err := csvWriter.Error(); err != nil { - fmt.Fprintf(w, "Error flushing CSV writer: %v\n", err) + if err := csvWriter.Error(); err != nil { + fmt.Fprintf(w, "Error flushing CSV writer: %v\n", err) + } } } diff --git a/datafetcher/handlers/transactions.go b/datafetcher/handlers/transactions.go index 5316df3..98c8c54 100644 --- a/datafetcher/handlers/transactions.go +++ b/datafetcher/handlers/transactions.go @@ -6,7 +6,6 @@ import ( "log" "net/http" "net/url" - "strconv" "github.com/a-h/templ" "github.com/esteanes/expense-manager/datafetcher/functions" @@ -35,30 +34,33 @@ func NewTransactionHandler(log *log.Logger, upclient *upclient.APIClient, auth c func (h *TransactionsHandler) Post(w http.ResponseWriter, r *http.Request) {} func (h *TransactionsHandler) Get(w http.ResponseWriter, r *http.Request) { - queryParams := r.URL.Query() - numTransactions, err := strconv.ParseInt(queryParams.Get(functions.TransactionNumQueryParam), 10, 32) - if err != nil { - numTransactions = int64(10) - } - transactionsChannel := make(chan upclient.TransactionResource, numTransactions) - accountId := queryParams.Get(functions.AccountIdQueryParam) - if accountId == "" { - go h.getTransactionsForAllAccounts(transactionsChannel, int32(numTransactions)) + queryParams := functions.FetchQueryParams(r.URL.Query()) + transactionsChannel := make(chan upclient.TransactionResource, *queryParams.NumTransactions) + if queryParams.AccountID == nil { + go h.getTransactionsForAllAccounts(transactionsChannel, queryParams) } else { - go h.getTransactionsForSpecifiedAccount(transactionsChannel, int32(numTransactions), accountId) + go h.getTransactionsForSpecifiedAccount(transactionsChannel, queryParams) } accountsChannel := make(chan upclient.AccountResource) go h.AccountHandler.GetAccounts(accountsChannel, upclient.OwnershipTypeEnum("INDIVIDUAL")) - templ.Handler(templates.Transactions("Transactions", transactionsChannel, accountsChannel, strconv.Itoa(int(numTransactions))), templ.WithStreaming()).ServeHTTP(w, r) + templ.Handler(templates.Transactions("Transactions", transactionsChannel, accountsChannel, queryParams), templ.WithStreaming()).ServeHTTP(w, r) } -func (h *TransactionsHandler) getTransactionsForAllAccounts(transactionsChannel chan upclient.TransactionResource, numTransactions int32) { +func (h *TransactionsHandler) getTransactionsForAllAccounts(transactionsChannel chan upclient.TransactionResource, queryParams *functions.QueryParams) { defer close(transactionsChannel) getRequest := h.UpClient.TransactionsAPI.TransactionsGet(h.UpAuth).PageSize(h.MaxPageSize) + + if queryParams.StartDate != nil { + getRequest.FilterSince(*queryParams.StartDate) + } + if queryParams.EndDate != nil { + getRequest.FilterUntil(*queryParams.EndDate) + } + var pageAfter *string pageAfter = nil countTransactions := int32(0) - for countTransactions < numTransactions { + for countTransactions < *queryParams.NumTransactions { if pageAfter != nil { pageKeyParsed, err := ExtractPageAfter(*pageAfter) if err != nil { @@ -80,7 +82,7 @@ func (h *TransactionsHandler) getTransactionsForAllAccounts(transactionsChannel h.Log.Println(fmt.Sprintf("page after link is: %s", *pageAfter)) } for _, transaction := range resp.Data { - if countTransactions < numTransactions { + if countTransactions < *queryParams.NumTransactions { transactionsChannel <- transaction countTransactions++ } @@ -91,13 +93,23 @@ func (h *TransactionsHandler) getTransactionsForAllAccounts(transactionsChannel } } -func (h *TransactionsHandler) getTransactionsForSpecifiedAccount(transactionsChannel chan upclient.TransactionResource, numTransactions int32, accountId string) { +func (h *TransactionsHandler) getTransactionsForSpecifiedAccount(transactionsChannel chan upclient.TransactionResource, queryParams *functions.QueryParams) { defer close(transactionsChannel) - getRequest := h.UpClient.TransactionsAPI.AccountsAccountIdTransactionsGet(h.UpAuth, accountId).PageSize(h.MaxPageSize) + getRequest := h.UpClient.TransactionsAPI.AccountsAccountIdTransactionsGet(h.UpAuth, *queryParams.AccountID).PageSize(h.MaxPageSize) + + if queryParams.StartDate != nil { + h.Log.Println(fmt.Sprintf("Setting Filter Since to: %s", *queryParams.StartDate)) + getRequest = getRequest.FilterSince(*queryParams.StartDate) + } + if queryParams.EndDate != nil { + h.Log.Println(fmt.Sprintf("Setting Filter Until to: %s", *queryParams.EndDate)) + getRequest = getRequest.FilterUntil(*queryParams.EndDate) + } + var pageAfter *string pageAfter = nil countTransactions := int32(0) - for countTransactions < numTransactions { + for countTransactions < *queryParams.NumTransactions { if pageAfter != nil { pageKeyParsed, err := ExtractPageAfter(*pageAfter) if err != nil { @@ -114,16 +126,17 @@ func (h *TransactionsHandler) getTransactionsForSpecifiedAccount(transactionsCha } return } - pageAfter = resp.Links.Next.Get() - if pageAfter != nil { - h.Log.Println(fmt.Sprintf("page after link is: %s", *pageAfter)) - } for _, transaction := range resp.Data { - if countTransactions < numTransactions { + if countTransactions < *queryParams.NumTransactions { transactionsChannel <- transaction countTransactions++ } } + pageAfter = resp.Links.Next.Get() + if pageAfter == nil { + break + } + h.Log.Println(fmt.Sprintf("page after link is: %s", *pageAfter)) } if pageAfter == nil { h.Log.Println("You have reached the end of all transactions") diff --git a/datafetcher/server.go b/datafetcher/server.go index 35eff41..2f19aee 100644 --- a/datafetcher/server.go +++ b/datafetcher/server.go @@ -80,7 +80,7 @@ func HandleRequests(upBankToken string, log *log.Logger) { // Creating individual handlers accountHandler := handlers.NewAccountHandler(log, apiClient, auth) transactionsHandler := handlers.NewTransactionHandler(log, apiClient, auth, accountHandler) - transactionsCsvHandler := handlers.NewTransactionCsvHandler(log, apiClient, auth) + transactionsCsvHandler := handlers.NewTransactionCsvHandler(log, apiClient, auth, transactionsHandler) staticFileHandler := handlers.NewStaticFileHandler(log) mux := http.NewServeMux() mux.HandleFunc(accountHandler.Uri, accountHandler.ServeHTTP) diff --git a/datafetcher/templates/transactions.templ b/datafetcher/templates/transactions.templ index 45e65a6..56016d9 100644 --- a/datafetcher/templates/transactions.templ +++ b/datafetcher/templates/transactions.templ @@ -1,6 +1,11 @@ package templates -import "github.com/esteanes/expense-manager/datafetcher/upclient" +import ( + "github.com/esteanes/expense-manager/datafetcher/functions" + "github.com/esteanes/expense-manager/datafetcher/upclient" + "strconv" + "time" +) func getMessage(transaction upclient.TransactionResource) string { maybeString := transaction.Attributes.Message.Get() @@ -39,9 +44,23 @@ func getCategory(transaction upclient.TransactionResource) string { } +func getDateOrDefault(dateTime *time.Time, defaultTime time.Time) string { + if dateTime == nil { + return defaultTime.Format("2006-01-02") + } + return dateTime.Format("2006-01-02") +} + templ TransactionsTable(transactions chan upclient.TransactionResource) {

Transactions

+
@@ -75,6 +94,53 @@ templ TransactionsTable(transactions chan upclient.TransactionResource) {
+ } templ numTransactionSlider(numTransactions string) { @@ -119,33 +185,32 @@ templ numTransactionSlider(numTransactions string) { } -templ dateSelector() { +templ dateSelector(startDate string, endDate string) {

Select Date Range

- +
- +
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -230,7 +242,7 @@ func numTransactionSlider(numTransactions string) templ.Component { var templ_7745c5c3_Var12 string templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(numTransactions) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `datafetcher/templates/transactions.templ`, Line: 84, Col: 77} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `datafetcher/templates/transactions.templ`, Line: 150, Col: 77} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { @@ -243,7 +255,7 @@ func numTransactionSlider(numTransactions string) templ.Component { var templ_7745c5c3_Var13 string templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(numTransactions) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `datafetcher/templates/transactions.templ`, Line: 88, Col: 64} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `datafetcher/templates/transactions.templ`, Line: 154, Col: 64} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { @@ -257,7 +269,7 @@ func numTransactionSlider(numTransactions string) templ.Component { }) } -func dateSelector() templ.Component { +func dateSelector(startDate string, endDate string) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) @@ -275,7 +287,33 @@ func dateSelector() templ.Component { templ_7745c5c3_Var14 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Select Date Range

") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Select Date Range

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -283,7 +321,7 @@ func dateSelector() templ.Component { }) } -func Transactions(title string, transactions chan upclient.TransactionResource, accounts chan upclient.AccountResource, numTransactions string) templ.Component { +func Transactions(title string, transactions chan upclient.TransactionResource, accounts chan upclient.AccountResource, queryParams *functions.QueryParams) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) @@ -296,12 +334,12 @@ func Transactions(title string, transactions chan upclient.TransactionResource, }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var15 := templ.GetChildren(ctx) - if templ_7745c5c3_Var15 == nil { - templ_7745c5c3_Var15 = templ.NopComponent + templ_7745c5c3_Var17 := templ.GetChildren(ctx) + if templ_7745c5c3_Var17 == nil { + templ_7745c5c3_Var17 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var16 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var18 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) if !templ_7745c5c3_IsBuffer { @@ -313,7 +351,7 @@ func Transactions(title string, transactions chan upclient.TransactionResource, }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var17 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var19 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) if !templ_7745c5c3_IsBuffer { @@ -333,7 +371,7 @@ func Transactions(title string, transactions chan upclient.TransactionResource, if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = numTransactionSlider(numTransactions).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = numTransactionSlider(strconv.Itoa(int(*queryParams.NumTransactions))).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -341,13 +379,13 @@ func Transactions(title string, transactions chan upclient.TransactionResource, if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = dateSelector().Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = dateSelector(getDateOrDefault(queryParams.StartDate, time.Now().AddDate(0, 0, -1)), getDateOrDefault(queryParams.EndDate, time.Now())).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } return templ_7745c5c3_Err }) - templ_7745c5c3_Err = GridOrganiser().Render(templ.WithChildren(ctx, templ_7745c5c3_Var17), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = GridOrganiser().Render(templ.WithChildren(ctx, templ_7745c5c3_Var19), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -361,7 +399,7 @@ func Transactions(title string, transactions chan upclient.TransactionResource, } return templ_7745c5c3_Err }) - templ_7745c5c3_Err = Base(title).Render(templ.WithChildren(ctx, templ_7745c5c3_Var16), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = Base(title).Render(templ.WithChildren(ctx, templ_7745c5c3_Var18), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/datafetcher/upclient/api_transactions.go b/datafetcher/upclient/api_transactions.go index a06ffc6..3e87563 100644 --- a/datafetcher/upclient/api_transactions.go +++ b/datafetcher/upclient/api_transactions.go @@ -13,6 +13,7 @@ package upclient import ( "bytes" "context" + "fmt" "io" "net/http" "net/url" @@ -37,6 +38,50 @@ type ApiAccountsAccountIdTransactionsGetRequest struct { filterTag *string } +func (r ApiAccountsAccountIdTransactionsGetRequest) String() string { + var ( + pageSize = "" + pageBefore = "" + pageAfter = "" + filterStatus = "" + filterSince = "" + filterUntil = "" + filterCategory = "" + filterTag = "" + ) + + // Dereference pointers if they are not nil + if r.pageSize != nil { + pageSize = fmt.Sprintf("%d", *r.pageSize) + } + if r.pageBefore != nil { + pageBefore = *r.pageBefore + } + if r.pageAfter != nil { + pageAfter = *r.pageAfter + } + if r.filterStatus != nil { + filterStatus = string(*r.filterStatus) + } + if r.filterSince != nil { + filterSince = r.filterSince.Format(time.RFC3339) + } + if r.filterUntil != nil { + filterUntil = r.filterUntil.Format(time.RFC3339) + } + if r.filterCategory != nil { + filterCategory = *r.filterCategory + } + if r.filterTag != nil { + filterTag = *r.filterTag + } + + return fmt.Sprintf( + "ApiAccountsAccountIdTransactionsGetRequest{ accountId: %s, pageSize: %s, pageBefore: %s, pageAfter: %s, filterStatus: %s, filterSince: %s, filterUntil: %s, filterCategory: %s, filterTag: %s}", + r.accountId, pageSize, pageBefore, pageAfter, filterStatus, filterSince, filterUntil, filterCategory, filterTag, + ) +} + // The number of records to return in each page. func (r ApiAccountsAccountIdTransactionsGetRequest) PageSize(pageSize int32) ApiAccountsAccountIdTransactionsGetRequest { r.pageSize = &pageSize diff --git a/static/css/output.css b/static/css/output.css index 84aed6d..3fbef76 100644 --- a/static/css/output.css +++ b/static/css/output.css @@ -629,6 +629,10 @@ video { margin-top: 1.5rem; } +.ml-2 { + margin-left: 0.5rem; +} + .block { display: block; } @@ -661,6 +665,10 @@ video { height: 100%; } +.h-5 { + height: 1.25rem; +} + .max-h-96 { max-height: 24rem; } @@ -677,6 +685,10 @@ video { width: 100%; } +.w-5 { + width: 1.25rem; +} + .min-w-full { min-width: 100%; } @@ -689,6 +701,16 @@ video { table-layout: auto; } +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.animate-spin { + animation: spin 1s linear infinite; +} + .cursor-pointer { cursor: pointer; } @@ -895,6 +917,14 @@ video { color: rgb(255 255 255 / var(--tw-text-opacity)); } +.opacity-25 { + opacity: 0.25; +} + +.opacity-75 { + opacity: 0.75; +} + .shadow-lg { --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);