diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index beb4ebd8..c1dc2a4b 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -22,6 +22,14 @@ jobs: uses: actions/setup-go@v5 with: go-version: '1.21.x' + - name: npm install + run: | + npm i + - name: Generate Tailwind CSS + # 🚨 Must come before `templ_generate` to ensure CSS hash in + # layout.templ is updated first, before go templates compile + run: | + npm run tailwind:prod - name: Generate templ code uses: './.github/actions/templ_generate' with: diff --git a/.github/workflows/deploy-feature.yml b/.github/workflows/deploy-feature.yml index a50c1490..574b71e2 100644 --- a/.github/workflows/deploy-feature.yml +++ b/.github/workflows/deploy-feature.yml @@ -28,6 +28,14 @@ jobs: uses: actions/setup-go@v5 with: go-version: '1.21.x' + - name: npm install + run: | + npm i + - name: Generate Tailwind CSS + # 🚨 Must come before `templ_generate` to ensure CSS hash in + # layout.templ is updated first, before go templates compile + run: | + npm run tailwind:prod - name: Generate templ code uses: './.github/actions/templ_generate' with: @@ -78,7 +86,7 @@ jobs: uses: './.github/actions/generate_cloudflare_locations_file' - name: Deploy AWS resources via SST run: | - npm i && npx sst deploy --stage ${{ steps.extract_branch.outputs.branch }} + npx sst deploy --stage ${{ steps.extract_branch.outputs.branch }} - name: Deploy to Cloudflare Workers with Wrangler uses: cloudflare/wrangler-action@v3.7.0 env: diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index c096d5df..ab9b05f0 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -22,6 +22,14 @@ jobs: uses: actions/setup-go@v5 with: go-version: '1.21.x' + - name: npm install + run: | + npm i + - name: Generate Tailwind CSS + # 🚨 Must come before `templ_generate` to ensure CSS hash in + # layout.templ is updated first, before go templates compile + run: | + npm run tailwind:prod - name: Generate templ code uses: './.github/actions/templ_generate' with: diff --git a/API_ENDPOINTS.md b/API_ENDPOINTS.md index 02dd7d2b..7eb5963d 100644 --- a/API_ENDPOINTS.md +++ b/API_ENDPOINTS.md @@ -26,7 +26,7 @@ curl -X POST https://devnear.me/api/users \ "role": "standard_user" }' - + ``` 2. Get User by ID @@ -62,7 +62,7 @@ curl -X PUT https://devnear.me/api/users/<:user_id> \ 4. Delete User ```bash -curl -X DELETE https://devnear.me/api/users/<:user_id> +curl -X DELETE https://devnear.me/api/users/<:user_id> ``` ## Purchasables @@ -161,7 +161,7 @@ curl -X POST https://v63ojpt121.execute-api.us-east-1.amazonaws.com/api/event-rs }' ``` -2. GET EventRsvp By PK +2. GET EventRsvp By PK ```bash curl -X GET https://v63ojpt121.execute-api.us-east-1.amazonaws.com/api/event-rsvps/6ce1be30-f700-475c-b84a-49af0c73f337/ea49a5f8-e27c-47b0-8237-6f6f380a048c \ -H "Content-Type: application/json" @@ -279,57 +279,5 @@ curl -X DELETE https://devnear.me/api/registration-fields/<:event_id> \ ``` -## Registrations - -#### Note eventId comes before userId in url params - -1. Create Registration -```bash -curl -X POST https://v63ojpt121.execute-api.us-east-1.amazonaws.com/api/registrations/62352e94-b34d-4ee7-a9d1-f1c8e404dec0/99413f71-bb0e-43c9-bc3a-fafc64c5c799 \ - -H "Content-Type: application/json" \ - -d '{ - "responses": [ - {"attendeeEmail": "me@meetnear.ne"}, - {"tShirtSize": "XL"} - ] - }' -``` -2. Get Registration by Primary Key -```bash -/api/registrations/{:event_id}/{:user_id} -curl -X GET https://v63ojpt121.execute-api.us-east-1.amazonaws.com/api/registrations/62352e94-b34d-4ee7-a9d1-f1c8e404dec0/c9413f71-bb0e-43c9-bc3a-fafc64c5c799 \ - -H "Content-Type: application/json" -``` - -3. Get Registration by EventId -```bash -curl -X GET https://v63ojpt121.execute-api.us-east-1.amazonaws.com/api/registrations/event/62352e94-b34d-4ee7-a9d1-f1c8e404dec0 \ - -H "Content-Type: application/json" -``` - -4. Get Registration by UserId -```bash -curl -X GET https://v63ojpt121.execute-api.us-east-1.amazonaws.com/api/registrations/user/c9413f71-bb0e-43c9-bc3a-fafc64c5c799 \ - -H "Content-Type: application/json" -``` - -5. Update registration (uses PK) -```bash -curl -X PUT https://v63ojpt121.execute-api.us-east-1.amazonaws.com/api/registrations/62352e94-b34d-4ee7-a9d1-f1c8e404dec0/c9413f71-bb0e-43c9-bc3a-fafc64c5c799 \ - -H "Content-Type: application/json" \ - -d '{ - "responses": [ - {"attendeeEmail": "newemail@meetnear.ne"}, - {"tShirtSize": "L"} - ] - }' -``` - -6. Delete Registration (uses PK) -```bash -curl -X DELETE https://v63ojpt121.execute-api.us-east-1.amazonaws.com/api/registrations/62352e94-b34d-4ee7-a9d1-f1c8e404dec0/c9413f71-bb0e-43c9-bc3a-fafc64c5c799 \ - -H "Content-Type: application/json" -``` - diff --git a/functions/gateway/handlers/data_handlers.go b/functions/gateway/handlers/data_handlers.go index cce830ea..36a45b2d 100644 --- a/functions/gateway/handlers/data_handlers.go +++ b/functions/gateway/handlers/data_handlers.go @@ -79,6 +79,12 @@ type rawEvent struct { HideCrossPromo *bool `json:"hideCrossPromo,omitempty"` } +// Create a new struct that includes the createPurchase fields and the Stripe checkout URL +type PurchaseResponse struct { + internal_types.PurchaseInsert + StripeCheckoutURL string `json:"stripe_checkout_url"` +} + func ConvertRawEventToEvent(raw rawEvent, requireId bool) (types.Event, error) { loc, err := time.LoadLocation(raw.Timezone) if err != nil { @@ -634,7 +640,9 @@ func CreateCheckoutSession(w http.ResponseWriter, r *http.Request) (err error) { } // all purchases are pending and a client passing this status should be overridden - createPurchase.Status = "PENDING" + if createPurchase.Status == "" { + createPurchase.Status = "PENDING" + } err = json.Unmarshal(body, &createPurchase) if err != nil { @@ -655,12 +663,51 @@ func CreateCheckoutSession(w http.ResponseWriter, r *http.Request) (err error) { createdAtString := fmt.Sprintf("%020d", _createdAt) // Pad with zeros to a fixed width of 20 digits createPurchase.CreatedAtString = createdAtString + referenceId := "event-" + eventId + "-user-" + userId + "-time-" + createPurchase.CreatedAtString + + // Create the composite key + compositeKey := fmt.Sprintf("%s_%s_%s", createPurchase.EventID, createPurchase.UserID, createPurchase.CreatedAtString) + + // Add the composite key and createdAt to the purchase object + createPurchase.CompositeKey = compositeKey + + // Create the purchase record immediately instead of deferring it + purchaseService := dynamodb_service.NewPurchaseService() + purchaseHandler := dynamodb_handlers.NewPurchaseHandler(purchaseService) + + // when there are no purchased items, we treat this as an "RSVP" or "INTERESTED" status that shows + // in the users purchase / registration history. The empty PurchasedItems array signals that this + // is an event that does not have `RegistrationFields` or `PurchasableItems` + if len(createPurchase.PurchasedItems) == 0 { + db := transport.GetDB() + log.Printf("createPurchase: %+v", createPurchase) + _, err := purchaseHandler.PurchaseService.InsertPurchase(r.Context(), db, createPurchase) + if err != nil { + transport.SendServerRes(w, []byte("Failed to insert free purchase into database: "+err.Error()), http.StatusInternalServerError, err) + return err + } + + // Create the response object + response := PurchaseResponse{ + PurchaseInsert: createPurchase, + StripeCheckoutURL: "", // Empty URL for free items + } + + // Marshal and send the response + purchaseJSON, err := json.Marshal(response) + if err != nil { + transport.SendServerRes(w, []byte("Failed to marshal purchase response: "+err.Error()), http.StatusInternalServerError, err) + return err + } + transport.SendServerRes(w, purchaseJSON, http.StatusOK, nil) + return nil + } purchasableService := dynamodb_service.NewPurchasableService() - h := dynamodb_handlers.NewPurchasableHandler(purchasableService) + purchasableHandler := dynamodb_handlers.NewPurchasableHandler(purchasableService) db := transport.GetDB() - purchasable, err := h.PurchasableService.GetPurchasablesByEventID(r.Context(), db, eventId) + purchasable, err := purchasableHandler.PurchasableService.GetPurchasablesByEventID(r.Context(), db, eventId) if err != nil { transport.SendServerRes(w, []byte("Failed to get purchasables for event id: "+eventId+" "+err.Error()), http.StatusInternalServerError, err) return @@ -683,13 +730,11 @@ func CreateCheckoutSession(w http.ResponseWriter, r *http.Request) (err error) { } } - // this boolean gets toggled in the scenario where stripe - // checkout instantiation or other unrelated checkout steps - // AFTER the inventory is officially "held" + optimistically - // decremented + // this boolean gets toggled in the scenario where stripe checkout fails to complete or other + // unrelated checkout failures AFTER the inventory is officially "held" + optimistically decremented var needsRevert bool - err = h.PurchasableService.UpdatePurchasableInventory(r.Context(), db, eventId, inventoryUpdates, purchasableMap) + err = purchasableHandler.PurchasableService.UpdatePurchasableInventory(r.Context(), db, eventId, inventoryUpdates, purchasableMap) if err != nil { transport.SendServerRes(w, []byte("Failed to update inventory: "+err.Error()), http.StatusInternalServerError, err) return @@ -706,13 +751,46 @@ func CreateCheckoutSession(w http.ResponseWriter, r *http.Request) (err error) { PurchasableIndex: update.PurchasableIndex, } } - revertErr := h.PurchasableService.UpdatePurchasableInventory(r.Context(), db, eventId, revertUpdates, purchasableMap) + revertErr := purchasableHandler.PurchasableService.UpdatePurchasableInventory(r.Context(), db, eventId, revertUpdates, purchasableMap) if revertErr != nil { log.Printf("ERR: Failed to revert inventory changes: %v", revertErr) } } }() + // Handle for free item purchases. These still need to track inventory and update the database, though we don't + // need to create a Stripe checkout session + if createPurchase.Total == 0 { + // Skip Stripe checkout for free items + createPurchase.Status = helpers.PurchaseStatus.Registered // Mark as registered immediately since it's free + + db := transport.GetDB() + log.Printf("createPurchase: %+v", createPurchase) + _, err := purchaseHandler.PurchaseService.InsertPurchase(r.Context(), db, createPurchase) + if err != nil { + needsRevert = true + transport.SendServerRes(w, []byte("Failed to insert free purchase into database: "+err.Error()), http.StatusInternalServerError, err) + return err + } + + // Create the response object + response := PurchaseResponse{ + PurchaseInsert: createPurchase, + StripeCheckoutURL: "", // Empty URL for free items + } + + // Marshal and send the response + purchaseJSON, err := json.Marshal(response) + if err != nil { + transport.SendServerRes(w, []byte("Failed to marshal purchase response: "+err.Error()), http.StatusInternalServerError, err) + return err + } + log.Printf("purchaseJSON: %s", string(purchaseJSON)) // Conve + transport.SendServerRes(w, purchaseJSON, http.StatusOK, nil) + return nil + } + + // Continue with existing Stripe checkout logic for paid items _, stripePrivKey := services.GetStripeKeyPair() stripe.Key = stripePrivKey @@ -736,10 +814,9 @@ func CreateCheckoutSession(w http.ResponseWriter, r *http.Request) (err error) { } } - referenceId := "event-" + eventId + "-user-" + userId + "-time-" + createPurchase.CreatedAtString params := &stripe.CheckoutSessionParams{ ClientReferenceID: stripe.String(referenceId), // Store purchase - SuccessURL: stripe.String(os.Getenv("APEX_URL") + "/event/" + eventId + "?checkout=success"), + SuccessURL: stripe.String(os.Getenv("APEX_URL") + "/admin/profile?new_purch_key=" + createPurchase.CompositeKey), CancelURL: stripe.String(os.Getenv("APEX_URL") + "/event/" + eventId + "?checkout=cancel"), LineItems: lineItems, // NOTE: `mode` needs to be "subscription" if there's a subscription / recurring item, @@ -764,19 +841,11 @@ func CreateCheckoutSession(w http.ResponseWriter, r *http.Request) (err error) { // Now that the checks are in place, we defer the transaction creation in the database // to respond to the client as quickly as possible defer func() { - purchaseService := dynamodb_service.NewPurchaseService() - h := dynamodb_handlers.NewPurchaseHandler(purchaseService) - createPurchase.Status = helpers.StripeCheckoutStatus.Pending - - // Create the composite key - compositeKey := fmt.Sprintf("%s_%s_%s", createPurchase.EventID, createPurchase.UserID, createPurchase.CreatedAtString) - - // Add the composite key and createdAt to the purchase object - createPurchase.CompositeKey = compositeKey + createPurchase.Status = helpers.PurchaseStatus.Pending log.Printf("db payload `createPurchase`: %+v", createPurchase) db := transport.GetDB() - _, err := h.PurchaseService.InsertPurchase(r.Context(), db, createPurchase) + _, err := purchaseHandler.PurchaseService.InsertPurchase(r.Context(), db, createPurchase) if err != nil { log.Printf("ERR: failed to insert purchase into purchases database for stripe session ID: %+v, err: %+v", stripeCheckoutResult.ID, err) } @@ -784,12 +853,6 @@ func CreateCheckoutSession(w http.ResponseWriter, r *http.Request) (err error) { log.Printf("\nstripe result: %+v", stripeCheckoutResult) - // Create a new struct that includes the createPurchase fields and the Stripe checkout URL - type PurchaseResponse struct { - internal_types.PurchaseInsert - StripeCheckoutURL string `json:"stripe_checkout_url"` - } - // Create the response object response := PurchaseResponse{ PurchaseInsert: createPurchase, @@ -895,7 +958,7 @@ func (h *PurchasableWebhookHandler) HandleCheckoutWebhook(w http.ResponseWriter, return err } purchaseUpdate := TransformPurchaseToUpdate(*purchase) - purchaseUpdate.Status = helpers.StripeCheckoutStatus.Settled + purchaseUpdate.Status = helpers.PurchaseStatus.Settled if checkoutSession.PaymentIntent != nil { purchaseUpdate.StripeTransactionId = checkoutSession.PaymentIntent.ID } @@ -980,7 +1043,7 @@ func (h *PurchasableWebhookHandler) HandleCheckoutWebhook(w http.ResponseWriter, return err } purchaseUpdate := TransformPurchaseToUpdate(*purchase) - purchaseUpdate.Status = helpers.StripeCheckoutStatus.Canceled + purchaseUpdate.Status = helpers.PurchaseStatus.Canceled _, err = h.PurchaseService.UpdatePurchase(r.Context(), db, eventID, userID, purchase.CreatedAtString, purchaseUpdate) if err != nil { diff --git a/functions/gateway/handlers/data_handlers_test.go b/functions/gateway/handlers/data_handlers_test.go index 9d6f5e73..2c75d8a6 100644 --- a/functions/gateway/handlers/data_handlers_test.go +++ b/functions/gateway/handlers/data_handlers_test.go @@ -1171,7 +1171,7 @@ func TestHandleCheckoutWebhook(t *testing.T) { EventID: eventId, UserID: userId, CreatedAtString: createdAt, - Status: helpers.StripeCheckoutStatus.Pending, + Status: helpers.PurchaseStatus.Pending, PurchasedItems: []internal_types.PurchasedItem{ { Name: "Test Item", @@ -1182,8 +1182,8 @@ func TestHandleCheckoutWebhook(t *testing.T) { }, nil }, UpdatePurchaseFunc: func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId, createdAt string, update internal_types.PurchaseUpdate) (*internal_types.Purchase, error) { - if update.Status != helpers.StripeCheckoutStatus.Settled { - t.Errorf("expected status %v, got %v", helpers.StripeCheckoutStatus.Settled, update.Status) + if update.Status != helpers.PurchaseStatus.Settled { + t.Errorf("expected status %v, got %v", helpers.PurchaseStatus.Settled, update.Status) } return nil, nil }, @@ -1357,7 +1357,7 @@ func TestHandleCheckoutWebhook(t *testing.T) { EventID: eventId, UserID: userId, CreatedAtString: createdAt, - Status: helpers.StripeCheckoutStatus.Pending, + Status: helpers.PurchaseStatus.Pending, PurchasedItems: []internal_types.PurchasedItem{ { Name: "Test Item", @@ -1368,8 +1368,8 @@ func TestHandleCheckoutWebhook(t *testing.T) { }, nil }, UpdatePurchaseFunc: func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId, createdAt string, update internal_types.PurchaseUpdate) (*internal_types.Purchase, error) { - if update.Status != helpers.StripeCheckoutStatus.Canceled { - t.Errorf("expected status %v, got %v", helpers.StripeCheckoutStatus.Canceled, update.Status) + if update.Status != helpers.PurchaseStatus.Canceled { + t.Errorf("expected status %v, got %v", helpers.PurchaseStatus.Canceled, update.Status) } return nil, nil }, diff --git a/functions/gateway/handlers/dynamodb_handlers/event_rsvp_handlers.go b/functions/gateway/handlers/dynamodb_handlers/event_rsvp_handlers.go deleted file mode 100644 index fd910660..00000000 --- a/functions/gateway/handlers/dynamodb_handlers/event_rsvp_handlers.go +++ /dev/null @@ -1,287 +0,0 @@ -package dynamodb_handlers - -import ( - "encoding/json" - "io" - "net/http" - "time" - - "github.com/gorilla/mux" - dynamodb_service "github.com/meetnearme/api/functions/gateway/services/dynamodb_service" - "github.com/meetnearme/api/functions/gateway/transport" - internal_types "github.com/meetnearme/api/functions/gateway/types" -) - -type EventRsvpHandler struct { - EventRsvpService internal_types.EventRsvpServiceInterface -} - -func NewEventRsvpHandler(eventRsvpService internal_types.EventRsvpServiceInterface) *EventRsvpHandler { - return &EventRsvpHandler{EventRsvpService: eventRsvpService} -} - - -func (h *EventRsvpHandler) CreateEventRsvp(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - eventId := vars["event_id"] - if eventId == "" { - transport.SendServerRes(w, []byte("Missing event ID"), http.StatusBadRequest, nil) - return - } - userId := vars["user_id"] - if userId == "" { - transport.SendServerRes(w, []byte("Missing user ID"), http.StatusBadRequest, nil) - return - } - - var createEventRsvp internal_types.EventRsvpInsert - body, err := io.ReadAll(r.Body) - if err != nil { - transport.SendServerRes(w, []byte("Failed to read request body: "+err.Error()), http.StatusBadRequest, err) - return - } - - err = json.Unmarshal(body, &createEventRsvp) - if err != nil { - transport.SendServerRes(w, []byte("Invalid JSON payload: "+err.Error()), http.StatusUnprocessableEntity, err) - return - } - - createEventRsvp.CreatedAt = time.Now() - createEventRsvp.UpdatedAt = time.Now() - createEventRsvp.EventID = eventId - createEventRsvp.UserID = userId - - err = validate.Struct(&createEventRsvp) - if err != nil { - transport.SendServerRes(w, []byte("Invalid body: "+err.Error()), http.StatusBadRequest, err) - return - } - - db := transport.GetDB() - res, err := h.EventRsvpService.InsertEventRsvp(r.Context(), db, createEventRsvp) - if err != nil { - transport.SendServerRes(w, []byte("Failed to create eventRsvp: "+err.Error()), http.StatusInternalServerError, err) - return - } - - response, err := json.Marshal(res) - if err != nil { - transport.SendServerRes(w, []byte("Error marshaling JSON"), http.StatusInternalServerError, err) - return - } - - transport.SendServerRes(w, response, http.StatusCreated, nil) -} - -func (h *EventRsvpHandler) GetEventRsvpByPk(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - eventId := vars["event_id"] - if eventId == "" { - transport.SendServerRes(w, []byte("Missing eventRsvp ID"), http.StatusBadRequest, nil) - return - } - userId := vars["user_id"] - if userId == "" { - transport.SendServerRes(w, []byte("Missing eventRsvp ID"), http.StatusBadRequest, nil) - return - } - - db := transport.GetDB() - eventRsvp, err := h.EventRsvpService.GetEventRsvpByPk(r.Context(), db, eventId, userId) - if err != nil { - transport.SendServerRes(w, []byte("Failed to get user: "+err.Error()), http.StatusInternalServerError, err) - return - } - - if eventRsvp == nil { - transport.SendServerRes(w, []byte("EventRsvp not found"), http.StatusNotFound, nil) - return - } - - response, err := json.Marshal(eventRsvp) - if err != nil { - transport.SendServerRes(w, []byte("Error marshaling JSON"), http.StatusInternalServerError, err) - return - } - - transport.SendServerRes(w, response, http.StatusOK, nil) -} - -func (h *EventRsvpHandler) GetEventRsvpsByUserID(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["user_id"] - if id == "" { - transport.SendServerRes(w, []byte("Missing user_id ID"), http.StatusBadRequest, nil) - return - } - - db := transport.GetDB() - users, err := h.EventRsvpService.GetEventRsvpsByUserID(r.Context(), db, id) - if err != nil { - transport.SendServerRes(w, []byte("Failed to get user's eventRsvps: "+err.Error()), http.StatusInternalServerError, err) - return - } - - response, err := json.Marshal(users) - if err != nil { - transport.SendServerRes(w, []byte("Error marshaling JSON"), http.StatusInternalServerError, err) - return - } - - transport.SendServerRes(w, response, http.StatusOK, nil) -} - -func (h *EventRsvpHandler) GetEventRsvpsByEventID(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["event_id"] - if id == "" { - transport.SendServerRes(w, []byte("Missing event_id ID"), http.StatusBadRequest, nil) - return - } - - db := transport.GetDB() - events, err := h.EventRsvpService.GetEventRsvpsByEventID(r.Context(), db, id) - if err != nil { - transport.SendServerRes(w, []byte("Failed to get user's eventRsvps: "+err.Error()), http.StatusInternalServerError, err) - return - } - - response, err := json.Marshal(events) - if err != nil { - transport.SendServerRes(w, []byte("Error marshaling JSON"), http.StatusInternalServerError, err) - return - } - - transport.SendServerRes(w, response, http.StatusOK, nil) -} - -func (h *EventRsvpHandler) UpdateEventRsvp(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - eventId := vars["event_id"] - if eventId == "" { - transport.SendServerRes(w, []byte("Missing eventRsvp ID"), http.StatusBadRequest, nil) - return - } - userId := vars["user_id"] - if userId == "" { - transport.SendServerRes(w, []byte("Missing eventRsvp ID"), http.StatusBadRequest, nil) - return - } - - var updateEventRsvp internal_types.EventRsvpUpdate - body, err := io.ReadAll(r.Body) - if err != nil { - transport.SendServerRes(w, []byte("Failed to read request body: "+err.Error()), http.StatusBadRequest, err) - return - } - - err = json.Unmarshal(body, &updateEventRsvp) - if err != nil { - transport.SendServerRes(w, []byte("Invalid JSON payload: "+err.Error()), http.StatusUnprocessableEntity, err) - return - } - - err = validate.Struct(&updateEventRsvp) - if err != nil { - transport.SendServerRes(w, []byte("Invalid body: "+err.Error()), http.StatusBadRequest, err) - return - } - - db := transport.GetDB() - user, err := h.EventRsvpService.UpdateEventRsvp(r.Context(), db, eventId, userId, updateEventRsvp) - if err != nil { - transport.SendServerRes(w, []byte("Failed to update eventRsvp: "+err.Error()), http.StatusInternalServerError, err) - return - } - - if user == nil { - transport.SendServerRes(w, []byte("EventRsvp not found"), http.StatusNotFound, nil) - return - } - - response, err := json.Marshal(user) - if err != nil { - transport.SendServerRes(w, []byte("Error marshaling JSON"), http.StatusInternalServerError, err) - return - } - - transport.SendServerRes(w, response, http.StatusOK, nil) -} - -func (h *EventRsvpHandler) DeleteEventRsvp(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - eventId := vars["event_id"] - if eventId == "" { - transport.SendServerRes(w, []byte("Missing event ID"), http.StatusBadRequest, nil) - return - } - userId := vars["user_id"] - if userId == "" { - transport.SendServerRes(w, []byte("Missing user ID"), http.StatusBadRequest, nil) - return - } - - db := transport.GetDB() - err := h.EventRsvpService.DeleteEventRsvp(r.Context(), db, eventId, userId) - if err != nil { - transport.SendServerRes(w, []byte("Failed to delete eventRsvp: "+err.Error()), http.StatusInternalServerError, err) - return - } - - transport.SendServerRes(w, []byte("EventRsvp successfully deleted"), http.StatusOK, nil) -} - -func CreateEventRsvpHandler(w http.ResponseWriter, r *http.Request) http.HandlerFunc { - eventRsvpService := dynamodb_service.NewEventRsvpService() - handler := NewEventRsvpHandler(eventRsvpService) - return func(w http.ResponseWriter, r *http.Request) { - handler.CreateEventRsvp(w, r) - } -} - - -// GetEventRsvpHandler is a wrapper that creates the UserHandler and returns the handler function for getting a eventRsvp by ID -func GetEventRsvpByPkHandler(w http.ResponseWriter, r *http.Request) http.HandlerFunc { - eventRsvpService := dynamodb_service.NewEventRsvpService() - handler := NewEventRsvpHandler(eventRsvpService) - return func(w http.ResponseWriter, r *http.Request) { - handler.GetEventRsvpByPk(w, r) - } -} - -// GetEventRsvpsHandler is a wrapper that creates the UserHandler and returns the handler function for getting all eventRsvps -func GetEventRsvpsByEventIDHandler(w http.ResponseWriter, r *http.Request) http.HandlerFunc { - eventRsvpService := dynamodb_service.NewEventRsvpService() - handler := NewEventRsvpHandler(eventRsvpService) - return func(w http.ResponseWriter, r *http.Request) { - handler.GetEventRsvpsByEventID(w, r) - } -} - -func GetEventRsvpsByUserIDHandler(w http.ResponseWriter, r *http.Request) http.HandlerFunc { - eventRsvpService := dynamodb_service.NewEventRsvpService() - handler := NewEventRsvpHandler(eventRsvpService) - return func(w http.ResponseWriter, r *http.Request) { - handler.GetEventRsvpsByUserID(w, r) - } -} - -// UpdateEventRsvpHandler is a wrapper that creates the UserHandler and returns the handler function for updating a eventRsvp -func UpdateEventRsvpHandler(w http.ResponseWriter, r *http.Request) http.HandlerFunc { - eventRsvpService := dynamodb_service.NewEventRsvpService() - handler := NewEventRsvpHandler(eventRsvpService) - return func(w http.ResponseWriter, r *http.Request) { - handler.UpdateEventRsvp(w, r) - } -} - -// DeleteEventRsvpHandler is a wrapper that creates the UserHandler and returns the handler function for deleting a eventRsvp -func DeleteEventRsvpHandler(w http.ResponseWriter, r *http.Request) http.HandlerFunc { - eventRsvpService := dynamodb_service.NewEventRsvpService() - handler := NewEventRsvpHandler(eventRsvpService) - return func(w http.ResponseWriter, r *http.Request) { - handler.DeleteEventRsvp(w, r) - } -} - diff --git a/functions/gateway/handlers/dynamodb_handlers/event_rsvp_handlers_test.go b/functions/gateway/handlers/dynamodb_handlers/event_rsvp_handlers_test.go deleted file mode 100644 index f31000c0..00000000 --- a/functions/gateway/handlers/dynamodb_handlers/event_rsvp_handlers_test.go +++ /dev/null @@ -1,175 +0,0 @@ -package dynamodb_handlers - -import ( - "context" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "bytes" - "encoding/json" - - "github.com/gorilla/mux" - dynamodb_service "github.com/meetnearme/api/functions/gateway/services/dynamodb_service" - internal_types "github.com/meetnearme/api/functions/gateway/types" -) - -// other imports remain unchanged - -// Modify your TestInsertEventRsvp function to include a request body -func TestInsertEventRsvp(t *testing.T) { - mockService := &dynamodb_service.MockEventRsvpService{ - InsertEventRsvpFunc: func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventRsvp internal_types.EventRsvpInsert) (*internal_types.EventRsvp, error) { - return &internal_types.EventRsvp{EventID: eventRsvp.EventID, UserID: eventRsvp.UserID}, nil - }, - } - - handler := NewEventRsvpHandler(mockService) - - // Constructing a JSON body - body := `{ - "event_id": "event123", - "user_id": "user123", - "event_source_type": "someType", - "event_source_id": "someSourceID", - "status": "someStatus" - }` - - req := httptest.NewRequest(http.MethodPost, "/rsvp/event123/user123", strings.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - req = mux.SetURLVars(req, map[string]string{"event_id": "event123", "user_id": "user123"}) - - w := httptest.NewRecorder() - handler.CreateEventRsvp(w, req) - - res := w.Result() - if res.StatusCode != http.StatusCreated { - t.Errorf("Expected status code 200, got %d", res.StatusCode) - } -} - -// TestUpdateEventRsvp tests updating an RSVP using a mock service. -func TestUpdateEventRsvp(t *testing.T) { - mockService := &dynamodb_service.MockEventRsvpService{ - UpdateEventRsvpFunc: func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId string, eventRsvp internal_types.EventRsvpUpdate) (*internal_types.EventRsvp, error) { - return &internal_types.EventRsvp{EventID: eventId, UserID: userId}, nil - }, - } - - handler := NewEventRsvpHandler(mockService) - - // Create a valid JSON payload - eventRsvp := internal_types.EventRsvpUpdate{ - // Populate this struct as needed for your test - } - payload, _ := json.Marshal(eventRsvp) // Handle the error properly in production code - - req := httptest.NewRequest(http.MethodPut, "/rsvp/event123/user123", bytes.NewBuffer(payload)) - req.Header.Set("Content-Type", "application/json") // Set the content type header - req = mux.SetURLVars(req, map[string]string{"event_id": "event123", "user_id": "user123"}) - - w := httptest.NewRecorder() - handler.UpdateEventRsvp(w, req) - - res := w.Result() - if res.StatusCode != http.StatusOK { - t.Errorf("Expected status code 200, got %d", res.StatusCode) - } -} - - -// TestGetEventRsvpByPk tests fetching RSVP by primary key using a mock service. -func TestGetEventRsvpByPk(t *testing.T) { - mockService := &dynamodb_service.MockEventRsvpService{ - GetEventRsvpByPkFunc: func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId string) (*internal_types.EventRsvp, error) { - return &internal_types.EventRsvp{EventID: eventId, UserID: userId}, nil - }, - } - - handler := NewEventRsvpHandler(mockService) - - req := httptest.NewRequest(http.MethodGet, "/rsvp/event123/user123", nil) - req = mux.SetURLVars(req, map[string]string{"event_id": "event123", "user_id": "user123"}) - - w := httptest.NewRecorder() - handler.GetEventRsvpByPk(w, req) - - res := w.Result() - if res.StatusCode != http.StatusOK { - t.Errorf("Expected status code 200, got %d", res.StatusCode) - } -} - -// TestGetEventRsvpsByUserID tests fetching RSVPs by user ID using a mock service. -func TestGetEventRsvpsByUserID(t *testing.T) { - mockService := &dynamodb_service.MockEventRsvpService{ - GetEventRsvpsByUserIDFunc: func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, userId string) ([]internal_types.EventRsvp, error) { - return []internal_types.EventRsvp{ - {EventID: "event123", UserID: userId}, - {EventID: "event456", UserID: userId}, - }, nil - }, - } - - handler := NewEventRsvpHandler(mockService) - - req := httptest.NewRequest(http.MethodGet, "/rsvp/user_id", nil) - req = mux.SetURLVars(req, map[string]string{"user_id": "user123"}) - - w := httptest.NewRecorder() - handler.GetEventRsvpsByUserID(w, req) - - res := w.Result() - if res.StatusCode != http.StatusOK { - t.Errorf("Expected status code 200, got %d", res.StatusCode) - } -} - -// TestGetEventRsvpsByEventID tests fetching RSVPs by event ID using a mock service. -func TestGetEventRsvpsByEventID(t *testing.T) { - mockService := &dynamodb_service.MockEventRsvpService{ - GetEventRsvpsByEventIDFunc: func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId string) ([]internal_types.EventRsvp, error) { - return []internal_types.EventRsvp{ - {EventID: eventId, UserID: "user123"}, - {EventID: eventId, UserID: "user456"}, - }, nil - }, - } - - handler := NewEventRsvpHandler(mockService) - - req := httptest.NewRequest(http.MethodGet, "/rsvp/event_id", nil) - req = mux.SetURLVars(req, map[string]string{"event_id": "event123"}) - - w := httptest.NewRecorder() - handler.GetEventRsvpsByEventID(w, req) - - res := w.Result() - if res.StatusCode != http.StatusOK { - t.Errorf("Expected status code 200, got %d", res.StatusCode) - } -} - -// TestDeleteEventRsvp tests deleting an RSVP using a mock service. -func TestDeleteEventRsvp(t *testing.T) { - mockService := &dynamodb_service.MockEventRsvpService{ - DeleteEventRsvpFunc: func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId string) error { - return nil - }, - } - - handler := NewEventRsvpHandler(mockService) - - req := httptest.NewRequest(http.MethodDelete, "/rsvp/event123/user123", nil) - req = mux.SetURLVars(req, map[string]string{"event_id": "event123", "user_id": "user123"}) - - w := httptest.NewRecorder() - handler.DeleteEventRsvp(w, req) - - res := w.Result() - if res.StatusCode != http.StatusOK { - t.Errorf("Expected status code 200, got %d", res.StatusCode) - } -} - diff --git a/functions/gateway/handlers/dynamodb_handlers/purchasables_handlers.go b/functions/gateway/handlers/dynamodb_handlers/purchasables_handlers.go index 53cc112d..6ac733ff 100644 --- a/functions/gateway/handlers/dynamodb_handlers/purchasables_handlers.go +++ b/functions/gateway/handlers/dynamodb_handlers/purchasables_handlers.go @@ -7,12 +7,15 @@ import ( "net/http" "time" + "github.com/go-playground/validator" "github.com/gorilla/mux" "github.com/meetnearme/api/functions/gateway/services/dynamodb_service" "github.com/meetnearme/api/functions/gateway/transport" internal_types "github.com/meetnearme/api/functions/gateway/types" ) +var validate *validator.Validate = validator.New() + type PurchasableHandler struct { PurchasableService internal_types.PurchasableServiceInterface } diff --git a/functions/gateway/handlers/dynamodb_handlers/purchase_handlers.go b/functions/gateway/handlers/dynamodb_handlers/purchase_handlers.go index a15aa734..0bcbfc7c 100644 --- a/functions/gateway/handlers/dynamodb_handlers/purchase_handlers.go +++ b/functions/gateway/handlers/dynamodb_handlers/purchase_handlers.go @@ -11,6 +11,7 @@ import ( dynamodb_types "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/gorilla/mux" "github.com/meetnearme/api/functions/gateway/helpers" + "github.com/meetnearme/api/functions/gateway/services" dynamodb_service "github.com/meetnearme/api/functions/gateway/services/dynamodb_service" "github.com/meetnearme/api/functions/gateway/transport" internal_types "github.com/meetnearme/api/functions/gateway/types" @@ -185,11 +186,48 @@ func (h *PurchaseHandler) GetPurchasesByUserID(w http.ResponseWriter, r *http.Re func (h *PurchaseHandler) GetPurchasesByEventID(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - id := vars["event_id"] - if id == "" { - transport.SendServerRes(w, []byte("Missing event_id ID"), http.StatusBadRequest, nil) + ctx := r.Context() + eventId := vars["event_id"] + if eventId == "" { + transport.SendServerRes(w, []byte("Missing event ID"), http.StatusBadRequest, nil) + return + } + + // Get user info from context + userInfo := helpers.UserInfo{} + if _, ok := ctx.Value("userInfo").(helpers.UserInfo); ok { + userInfo = ctx.Value("userInfo").(helpers.UserInfo) + } + userId := userInfo.Sub + if userId == "" { + transport.SendServerRes(w, []byte("You must be logged in to view this event's purchases"), http.StatusUnauthorized, nil) + return + } + + roleClaims := []helpers.RoleClaim{} + if _, ok := ctx.Value("roleClaims").([]helpers.RoleClaim); ok { + roleClaims = ctx.Value("roleClaims").([]helpers.RoleClaim) + } + // Validate event ownership + marqoClient, err := services.GetMarqoClient() + if err != nil { + transport.SendServerRes(w, []byte("Failed to get Marqo client: "+err.Error()), http.StatusInternalServerError, err) + return + } + event, err := services.GetMarqoEventByID(marqoClient, eventId, "") + if err != nil { + transport.SendServerRes(w, []byte("Failed to get event: "+err.Error()), http.StatusInternalServerError, err) return } + + canEdit := helpers.CanEditEvent(event, &userInfo, roleClaims) + + if !canEdit { + transport.SendServerRes(w, []byte("You are not authorized to view this event's purchases"), http.StatusForbidden, nil) + return + } + + // Handle pagination limit := r.URL.Query().Get("limit") limitInt, err := strconv.ParseInt(limit, 10, 32) if err != nil || limit == "" { @@ -198,7 +236,7 @@ func (h *PurchaseHandler) GetPurchasesByEventID(w http.ResponseWriter, r *http.R startKey := r.URL.Query().Get("start_key") db := transport.GetDB() - purchases, lastEvaluatedKey, err := h.PurchaseService.GetPurchasesByEventID(r.Context(), db, id, int32(limitInt), startKey) + purchases, lastEvaluatedKey, err := h.PurchaseService.GetPurchasesByEventID(r.Context(), db, eventId, int32(limitInt), startKey) if err != nil { transport.SendServerRes(w, []byte("Failed to get event's purchases: "+err.Error()), http.StatusInternalServerError, err) return diff --git a/functions/gateway/handlers/dynamodb_handlers/registration_handlers_test.go b/functions/gateway/handlers/dynamodb_handlers/purchase_handlers_test.go similarity index 56% rename from functions/gateway/handlers/dynamodb_handlers/registration_handlers_test.go rename to functions/gateway/handlers/dynamodb_handlers/purchase_handlers_test.go index 9c46992e..e55badf2 100644 --- a/functions/gateway/handlers/dynamodb_handlers/registration_handlers_test.go +++ b/functions/gateway/handlers/dynamodb_handlers/purchase_handlers_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" "net/http/httptest" "os" @@ -16,10 +17,11 @@ import ( "github.com/meetnearme/api/functions/gateway/helpers" dynamodb_service "github.com/meetnearme/api/functions/gateway/services/dynamodb_service" "github.com/meetnearme/api/functions/gateway/test_helpers" + "github.com/meetnearme/api/functions/gateway/types" internal_types "github.com/meetnearme/api/functions/gateway/types" ) -func TestGetRegistrationsByEventID(t *testing.T) { +func TestGetPurchasesByEventID(t *testing.T) { os.Setenv("GO_ENV", helpers.GO_TEST_ENV) defer os.Unsetenv("GO_ENV") // Save original environment variables @@ -52,7 +54,6 @@ func TestGetRegistrationsByEventID(t *testing.T) { testEventDescription = "This is a test event" ) - t.Log("54 << got to") loc, _ := time.LoadLocation("America/New_York") testEventStartTime, tmErr := helpers.UtcToUnix64("2099-05-01T12:00:00Z", loc) if tmErr != nil || testEventStartTime == 0 { @@ -81,7 +82,6 @@ func TestGetRegistrationsByEventID(t *testing.T) { w.WriteHeader(http.StatusOK) w.Write(responseBytes) })) - t.Log("84 << got to") // Set up mock Marqo server mockMarqoServer.Listener.Close() listener, err := test_helpers.BindToPort(t, testMarqoEndpoint) @@ -96,7 +96,6 @@ func TestGetRegistrationsByEventID(t *testing.T) { boundAddress := mockMarqoServer.Listener.Addr().String() os.Setenv("DEV_MARQO_API_BASE_URL", fmt.Sprintf("http://%s", boundAddress)) - t.Log("90 << got to") tests := []struct { name string userID string @@ -113,21 +112,19 @@ func TestGetRegistrationsByEventID(t *testing.T) { name: "unauthorized user", userID: "unauthorized_user", expectedCode: http.StatusForbidden, - expectedError: "You are not authorized to view this event's registrations", + expectedError: "You are not authorized to view this event's purchases", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Log("113 << got to") - mockService := &dynamodb_service.MockRegistrationService{ - GetRegistrationsByEventIDFunc: func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId string, limit int32, startKey string) ([]internal_types.Registration, map[string]dynamodb_types.AttributeValue, error) { - return []internal_types.Registration{{EventId: eventId, UserId: "user1"}}, nil, nil + mockService := &dynamodb_service.MockPurchaseService{ + GetPurchasesByEventIDFunc: func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId string, limit int32, startKey string) ([]internal_types.Purchase, map[string]dynamodb_types.AttributeValue, error) { + return []internal_types.Purchase{{EventID: eventId, UserID: "user1"}}, nil, nil }, } - handler := NewRegistrationHandler(mockService) - t.Log("120 << got to") - req := httptest.NewRequest(http.MethodGet, "/registrations/event_id", nil) + handler := NewPurchaseHandler(mockService) + req := httptest.NewRequest(http.MethodGet, "/purchases/event_id", nil) req = mux.SetURLVars(req, map[string]string{"event_id": testEventID}) // Add authentication context with test user @@ -138,62 +135,112 @@ func TestGetRegistrationsByEventID(t *testing.T) { req = req.WithContext(ctx) w := httptest.NewRecorder() - handler.GetRegistrationsByEventID(w, req) - t.Log("133 << got to") + handler.GetPurchasesByEventID(w, req) res := w.Result() if res.StatusCode != tt.expectedCode { t.Errorf("Expected status code %d, got %d", tt.expectedCode, res.StatusCode) } + responseBody, _ := io.ReadAll(res.Body) // If we expect an error, verify the error message if tt.expectedError != "" { - var response map[string]interface{} - if err := json.NewDecoder(res.Body).Decode(&response); err != nil { - t.Fatalf("Failed to decode response: %v", err) + if !strings.Contains(string(responseBody), tt.expectedError) { + t.Errorf("Expected error message to contain %q, got %q", tt.expectedError, string(responseBody)) } + } + }) + } +} - if msg, ok := response["error"].(map[string]interface{})["message"].(string); !ok || - !strings.Contains(msg, tt.expectedError) { - t.Errorf("Expected error message to contain %q, got %q", tt.expectedError, msg) +func TestGetPurchasesByUserID(t *testing.T) { + tests := []struct { + name string + requestUserID string // user ID in the request path + contextUserID string // user ID in the context + expectedCode int + expectedError string + }{ + { + name: "authorized user", + requestUserID: "test_user_123", + contextUserID: "test_user_123", + expectedCode: http.StatusOK, + }, + { + name: "unauthorized user", + requestUserID: "test_user_123", + contextUserID: "different_user", + expectedCode: http.StatusForbidden, + expectedError: "You are not authorized to view this user's purchases", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockService := &dynamodb_service.MockPurchaseService{ + GetPurchasesByUserIDFunc: func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, userId string, limit int32, startKey string) ([]internal_types.Purchase, map[string]dynamodb_types.AttributeValue, error) { + return []internal_types.Purchase{{UserID: userId}}, nil, nil + }, + } + handler := NewPurchaseHandler(mockService) + req := httptest.NewRequest(http.MethodGet, "/purchases/user_id", nil) + req = mux.SetURLVars(req, map[string]string{"user_id": tt.requestUserID}) + + // Add authentication context with test user + userInfo := helpers.UserInfo{ + Sub: tt.contextUserID, + } + ctx := context.WithValue(req.Context(), "userInfo", userInfo) + req = req.WithContext(ctx) + + w := httptest.NewRecorder() + handler.GetPurchasesByUserID(w, req) + res := w.Result() + if res.StatusCode != tt.expectedCode { + t.Errorf("Expected status code %d, got %d", tt.expectedCode, res.StatusCode) + } + responseBody, _ := io.ReadAll(res.Body) + // If we expect an error, verify the error message + if tt.expectedError != "" { + if !strings.Contains(string(responseBody), tt.expectedError) { + t.Errorf("Expected error message to contain %q, got %q", tt.expectedError, string(responseBody)) } } }) } } -func TestGetRegistrationsByUserID(t *testing.T) { - mockService := &dynamodb_service.MockRegistrationService{ - GetRegistrationsByUserIDFunc: func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, userId string) ([]internal_types.Registration, error) { - return []internal_types.Registration{{EventId: "event1", UserId: userId}}, nil +func TestDeletePurchase(t *testing.T) { + mockService := &dynamodb_service.MockPurchaseService{ + DeletePurchaseFunc: func(ctx context.Context, dynamodbClient types.DynamoDBAPI, eventId string, userId string) error { + return nil }, } - handler := NewRegistrationHandler(mockService) + handler := NewPurchaseHandler(mockService) + + // Add user context + const ( + testEventID = "event-123" + testUserID = "user-456" + ) - // Create request with user_id in path params - req := httptest.NewRequest(http.MethodGet, "/registrations/user123", nil) - req = mux.SetURLVars(req, map[string]string{"user_id": "user123"}) + req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/purchases/%s/%s", testEventID, testUserID), nil) + req = mux.SetURLVars(req, map[string]string{ + "event_id": testEventID, + "user_id": testUserID, + }) - // Create context with user info + // Add user context userInfo := helpers.UserInfo{ - Sub: "user123", // This should match the user_id in path params - // Add other required UserInfo fields if needed + Sub: testUserID, // Using the same user ID to test authorized deletion } ctx := context.WithValue(req.Context(), "userInfo", userInfo) req = req.WithContext(ctx) w := httptest.NewRecorder() - handler.GetRegistrationsByUserID(w, req) + handler.DeletePurchase(w, req) res := w.Result() if res.StatusCode != http.StatusOK { t.Errorf("Expected status code 200, got %d", res.StatusCode) } - - // Optionally verify response body - var response []internal_types.Registration - if err := json.NewDecoder(res.Body).Decode(&response); err != nil { - t.Fatalf("Failed to decode response: %v", err) - } - if len(response) != 1 || response[0].UserId != "user123" { - t.Errorf("Unexpected response content") - } } diff --git a/functions/gateway/handlers/dynamodb_handlers/registration_handlers.go b/functions/gateway/handlers/dynamodb_handlers/registration_handlers.go deleted file mode 100644 index 772e9392..00000000 --- a/functions/gateway/handlers/dynamodb_handlers/registration_handlers.go +++ /dev/null @@ -1,370 +0,0 @@ -package dynamodb_handlers - -import ( - "encoding/json" - "io" - "log" - "net/http" - "strconv" - "time" - - dynamodb_types "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/go-playground/validator" - "github.com/gorilla/mux" - "github.com/meetnearme/api/functions/gateway/helpers" - "github.com/meetnearme/api/functions/gateway/services" - dynamodb_service "github.com/meetnearme/api/functions/gateway/services/dynamodb_service" - "github.com/meetnearme/api/functions/gateway/transport" - internal_types "github.com/meetnearme/api/functions/gateway/types" -) - -// Validator instance for struct validation -var validate *validator.Validate = validator.New() - -func init() { - db = transport.CreateDbClient() -} - -// UserHandler handles user-related requests -type RegistrationHandler struct { - RegistrationService internal_types.RegistrationServiceInterface -} - -// NewRegistrationHandler creates a new RegistrationHandler with the given RegistrationService -func NewRegistrationHandler(registrationService internal_types.RegistrationServiceInterface) *RegistrationHandler { - return &RegistrationHandler{RegistrationService: registrationService} -} - -func (h *RegistrationHandler) CreateRegistration(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - eventId := vars["event_id"] - if eventId == "" { - transport.SendServerRes(w, []byte("Missing event ID"), http.StatusBadRequest, nil) - return - } - userId := vars["user_id"] - if userId == "" { - transport.SendServerRes(w, []byte("Missing user ID"), http.StatusBadRequest, nil) - return - } - - var createRegistration internal_types.RegistrationInsert - body, err := io.ReadAll(r.Body) - if err != nil { - transport.SendServerRes(w, []byte("Failed to read request body: "+err.Error()), http.StatusBadRequest, err) - return - } - - err = json.Unmarshal(body, &createRegistration) - if err != nil { - transport.SendServerRes(w, []byte("Invalid JSON payload: "+err.Error()), http.StatusUnprocessableEntity, err) - return - } - - createRegistration.CreatedAt = time.Now() - createRegistration.UpdatedAt = time.Now() - createRegistration.EventId = eventId - createRegistration.UserId = userId - - err = validate.Struct(&createRegistration) - if err != nil { - transport.SendServerRes(w, []byte("Invalid body: "+err.Error()), http.StatusBadRequest, err) - return - } - - res, err := h.RegistrationService.InsertRegistration(r.Context(), db, createRegistration, eventId, userId) - if err != nil { - transport.SendServerRes(w, []byte("Failed to create registration fields: "+err.Error()), http.StatusInternalServerError, err) - return - } - - response, err := json.Marshal(res) - if err != nil { - transport.SendServerRes(w, []byte("Error marshaling JSON"), http.StatusInternalServerError, err) - return - } - - transport.SendServerRes(w, response, http.StatusCreated, nil) -} - -func (h *RegistrationHandler) GetRegistrationByPk(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - eventId := vars["event_id"] - if eventId == "" { - transport.SendServerRes(w, []byte("Missing event ID"), http.StatusBadRequest, nil) - return - } - - userId := vars["user_id"] - if userId == "" { - transport.SendServerRes(w, []byte("Missing user ID"), http.StatusBadRequest, nil) - return - } - - registration, err := h.RegistrationService.GetRegistrationByPk(r.Context(), db, eventId, userId) - if err != nil { - transport.SendServerRes(w, []byte("Failed to get registrations: "+err.Error()), http.StatusInternalServerError, err) - return - } - - response, err := json.Marshal(registration) - if err != nil { - transport.SendServerRes(w, []byte("Error marshaling JSON"), http.StatusInternalServerError, err) - return - } - - transport.SendServerRes(w, response, http.StatusOK, nil) -} - -// This needs to change for use cases of fetching multiple users based on org ID or other -func (h *RegistrationHandler) GetRegistrationsByEventID(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - ctx := r.Context() - eventId := vars["event_id"] - if eventId == "" { - transport.SendServerRes(w, []byte("Missing event ID"), http.StatusBadRequest, nil) - return - } - - // Get user info from context - userInfo := helpers.UserInfo{} - if _, ok := ctx.Value("userInfo").(helpers.UserInfo); ok { - userInfo = ctx.Value("userInfo").(helpers.UserInfo) - } - userId := userInfo.Sub - if userId == "" { - transport.SendServerRes(w, []byte("You must be logged in to view this event's registrations"), http.StatusUnauthorized, nil) - return - } - - roleClaims := []helpers.RoleClaim{} - if _, ok := ctx.Value("roleClaims").([]helpers.RoleClaim); ok { - roleClaims = ctx.Value("roleClaims").([]helpers.RoleClaim) - } - // Validate event ownership - marqoClient, err := services.GetMarqoClient() - if err != nil { - transport.SendServerRes(w, []byte("Failed to get Marqo client: "+err.Error()), http.StatusInternalServerError, err) - return - } - event, err := services.GetMarqoEventByID(marqoClient, eventId, "") - if err != nil { - transport.SendServerRes(w, []byte("Failed to get event: "+err.Error()), http.StatusInternalServerError, err) - return - } - - canEdit := helpers.CanEditEvent(event, &userInfo, roleClaims) - - if !canEdit { - transport.SendServerRes(w, []byte("You are not authorized to view this event's registrations"), http.StatusForbidden, nil) - return - } - - // Handle pagination - limit := r.URL.Query().Get("limit") - limitInt, err := strconv.ParseInt(limit, 10, 32) - if err != nil || limit == "" { - limitInt = helpers.DEFAULT_PAGINATION_LIMIT - } - startKey := r.URL.Query().Get("start_key") - - // Get registrations - db := transport.GetDB() - registrations, lastEvaluatedKey, err := h.RegistrationService.GetRegistrationsByEventID(ctx, db, eventId, int32(limitInt), startKey) - if err != nil { - transport.SendServerRes(w, []byte("Failed to get registrations: "+err.Error()), http.StatusInternalServerError, err) - return - } - responseData := struct { - Count int `json:"count"` - NextKey map[string]dynamodb_types.AttributeValue `json:"nextKey"` - Registrations []internal_types.Registration `json:"registrations"` - }{ - Count: len(registrations), - NextKey: lastEvaluatedKey, - Registrations: registrations, - } - - response, err := json.Marshal(responseData) - if err != nil { - transport.SendServerRes(w, []byte("Error marshaling JSON"), http.StatusInternalServerError, err) - return - } - - transport.SendServerRes(w, response, http.StatusOK, nil) -} - -func (h *RegistrationHandler) GetRegistrationsByUserID(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - ctx := r.Context() - userId := vars["user_id"] - if userId == "" { - transport.SendServerRes(w, []byte("Missing event ID"), http.StatusBadRequest, nil) - return - } - - // Get user info from context - userInfo := helpers.UserInfo{} - if _, ok := ctx.Value("userInfo").(helpers.UserInfo); ok { - userInfo = ctx.Value("userInfo").(helpers.UserInfo) - } - _userId := userInfo.Sub - - if _userId == "" { - transport.SendServerRes(w, []byte("You must be loggged in to get your registrations"), http.StatusBadRequest, nil) - return - } - - if _userId != userId { - transport.SendServerRes(w, []byte("You are not authorized to view this user's registrations"), http.StatusForbidden, nil) - return - } - - registration, err := h.RegistrationService.GetRegistrationsByUserID(r.Context(), db, userId) - if err != nil { - transport.SendServerRes(w, []byte("Failed to get registrations: "+err.Error()), http.StatusInternalServerError, err) - return - } - - response, err := json.Marshal(registration) - if err != nil { - transport.SendServerRes(w, []byte("Error marshaling JSON"), http.StatusInternalServerError, err) - return - } - - transport.SendServerRes(w, response, http.StatusOK, nil) -} - -func (h *RegistrationHandler) UpdateRegistration(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - eventId := vars["event_id"] - if eventId == "" { - transport.SendServerRes(w, []byte("Missing event ID"), http.StatusBadRequest, nil) - return - } - - userId := vars["user_id"] - if userId == "" { - transport.SendServerRes(w, []byte("Missing user ID"), http.StatusBadRequest, nil) - return - } - - var updateRegistration internal_types.RegistrationUpdate - body, err := io.ReadAll(r.Body) - if err != nil { - transport.SendServerRes(w, []byte("Failed to read request body: "+err.Error()), http.StatusBadRequest, err) - return - } - - updateRegistration.UpdatedAt = time.Now() - - err = json.Unmarshal(body, &updateRegistration) - if err != nil { - transport.SendServerRes(w, []byte("Invalid JSON payload: "+err.Error()), http.StatusUnprocessableEntity, err) - return - } - - updateRegistration.UserId = userId - updateRegistration.EventId = eventId - - err = validate.Struct(&updateRegistration) - if err != nil { - transport.SendServerRes(w, []byte("Invalid body: "+err.Error()), http.StatusBadRequest, err) - return - } - - updatedRegistration, err := h.RegistrationService.UpdateRegistration(r.Context(), db, eventId, userId, updateRegistration) - if err != nil { - transport.SendServerRes(w, []byte("Failed to update user: "+err.Error()), http.StatusInternalServerError, err) - return - } - - if updatedRegistration == nil { - transport.SendServerRes(w, []byte("Registration not found"), http.StatusNotFound, nil) - return - } - - response, err := json.Marshal(updatedRegistration) - if err != nil { - transport.SendServerRes(w, []byte("Error marshaling JSON"), http.StatusInternalServerError, err) - return - } - - transport.SendServerRes(w, response, http.StatusOK, nil) -} - -func (h *RegistrationHandler) DeleteRegistration(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - eventId := vars["event_id"] - if eventId == "" { - transport.SendServerRes(w, []byte("Missing user ID"), http.StatusBadRequest, nil) - return - } - - userId := vars["user_id"] - if userId == "" { - transport.SendServerRes(w, []byte("Missing user ID"), http.StatusBadRequest, nil) - return - } - - err := h.RegistrationService.DeleteRegistration(r.Context(), db, eventId, userId) - if err != nil { - transport.SendServerRes(w, []byte("Failed to delete user: "+err.Error()), http.StatusInternalServerError, err) - return - } - - transport.SendServerRes(w, []byte("Registration successfully deleted"), http.StatusOK, nil) -} - -func CreateRegistrationHandler(w http.ResponseWriter, r *http.Request) http.HandlerFunc { - log.Printf("in reg fields wrapper") - registrationService := dynamodb_service.NewRegistrationService() - handler := NewRegistrationHandler(registrationService) - return func(w http.ResponseWriter, r *http.Request) { - handler.CreateRegistration(w, r) - } -} - -// GetRegistrationsHandler is a wrapper that creates the RegistrationHandler and returns the handler function for getting all users -func GetRegistrationByPkHandler(w http.ResponseWriter, r *http.Request) http.HandlerFunc { - registrationService := dynamodb_service.NewRegistrationService() - handler := NewRegistrationHandler(registrationService) - return func(w http.ResponseWriter, r *http.Request) { - handler.GetRegistrationByPk(w, r) - } -} - -// GetRegistrationsHandler is a wrapper that creates the RegistrationHandler and returns the handler function for getting all users -func GetRegistrationsByEventIDHandler(w http.ResponseWriter, r *http.Request) http.HandlerFunc { - registrationService := dynamodb_service.NewRegistrationService() - handler := NewRegistrationHandler(registrationService) - return func(w http.ResponseWriter, r *http.Request) { - handler.GetRegistrationsByEventID(w, r) - } -} - -func GetRegistrationsByUserIDHandler(w http.ResponseWriter, r *http.Request) http.HandlerFunc { - registrationService := dynamodb_service.NewRegistrationService() - handler := NewRegistrationHandler(registrationService) - return func(w http.ResponseWriter, r *http.Request) { - handler.GetRegistrationsByUserID(w, r) - } -} - -// UpdateRegistrationHandler is a wrapper that creates the RegistrationHandler and returns the handler function for updating a user -func UpdateRegistrationHandler(w http.ResponseWriter, r *http.Request) http.HandlerFunc { - registrationService := dynamodb_service.NewRegistrationService() - handler := NewRegistrationHandler(registrationService) - return func(w http.ResponseWriter, r *http.Request) { - handler.UpdateRegistration(w, r) - } -} - -// DeleteRegistrationHandler is a wrapper that creates the RegistrationHandler and returns the handler function for deleting a user -func DeleteRegistrationHandler(w http.ResponseWriter, r *http.Request) http.HandlerFunc { - registrationService := dynamodb_service.NewRegistrationService() - handler := NewRegistrationHandler(registrationService) - return func(w http.ResponseWriter, r *http.Request) { - handler.DeleteRegistration(w, r) - } -} diff --git a/functions/gateway/handlers/page_handlers.go b/functions/gateway/handlers/page_handlers.go index 22878e06..c00cfb37 100644 --- a/functions/gateway/handlers/page_handlers.go +++ b/functions/gateway/handlers/page_handlers.go @@ -463,7 +463,7 @@ func GetEventDetailsPage(w http.ResponseWriter, r *http.Request) http.HandlerFun canEdit := helpers.CanEditEvent(event, &userInfo, roleClaims) checkoutParamVal := r.URL.Query().Get("checkout") - eventDetailsPage := pages.EventDetailsPage(*event, checkoutParamVal, canEdit) + eventDetailsPage := pages.EventDetailsPage(*event, userInfo, checkoutParamVal, canEdit) layoutTemplate := pages.Layout(helpers.SitePages["event-detail"], userInfo, eventDetailsPage, *event) var buf bytes.Buffer err = layoutTemplate.Render(ctx, &buf) diff --git a/functions/gateway/helpers/constants.go b/functions/gateway/helpers/constants.go index f8b8e868..c294ff67 100644 --- a/functions/gateway/helpers/constants.go +++ b/functions/gateway/helpers/constants.go @@ -76,16 +76,20 @@ type UserInfo struct { Metadata string `json:"metadata"` } -type StripeCheckoutStatuses struct { - Settled string - Pending string - Canceled string +type PurchaseStatuses struct { + Settled string + Pending string + Canceled string + Registered string + Interested string } -var StripeCheckoutStatus = StripeCheckoutStatuses{ - Settled: "SETTLED", - Pending: "PENDING", - Canceled: "CANCELED", +var PurchaseStatus = PurchaseStatuses{ + Settled: "SETTLED", + Pending: "PENDING", + Canceled: "CANCELED", + Registered: "REGISTERED", + Interested: "INTERESTED", } // RoleClaim represents a formatted role claim. @@ -134,16 +138,18 @@ type SitePage struct { } var SitePages = map[string]SitePage{ + // NOTE: the {trailingslash:\\/?} is required for a route to match with or without a trailing slash, the + // solution is from this github comment (see discussion as well) https://github.com/gorilla/mux/issues/30#issuecomment-1666428538 "home": {Key: "home", Slug: "/", Name: "Home", SubnavItems: []string{SubnavItems[NvMain], SubnavItems[NvFilters]}}, - "about": {Key: "about", Slug: "/about", Name: "About", SubnavItems: []string{SubnavItems[NvMain]}}, - "profile": {Key: "profile", Slug: "/admin/profile", Name: "Profile", SubnavItems: []string{SubnavItems[NvMain]}}, - "add-event-source": {Key: "add-event-source", Slug: "/admin/add-event-source", Name: "Add Event Source", SubnavItems: []string{SubnavItems[NvMain]}}, - "settings": {Key: "settings", Slug: "/admin/profile/settings", Name: "Settings", SubnavItems: []string{SubnavItems[NvMain]}}, - "map-embed": {Key: "map-embed", Slug: "/map-embed", Name: "MapEmbed", SubnavItems: []string{SubnavItems[NvMain]}}, - "event-detail": {Key: "event-detail", Slug: "/event/{" + EVENT_ID_KEY + "}", Name: "Event Detail", SubnavItems: []string{SubnavItems[NvMain], SubnavItems[NvCart]}}, - "add-event": {Key: "add-event", Slug: "/admin/event/new", Name: "Add Event", SubnavItems: []string{SubnavItems[NvMain]}}, - "edit-event": {Key: "edit-event", Slug: "/admin/event/{" + EVENT_ID_KEY + "}/edit", Name: "Edit Event", SubnavItems: []string{SubnavItems[NvMain]}}, - "attendees-event": {Key: "attendees-event", Slug: "/admin/event/{" + EVENT_ID_KEY + "}/attendees", Name: "Event Attendees", SubnavItems: []string{SubnavItems[NvMain]}}, + "about": {Key: "about", Slug: "/about{trailingslash:\\/?}", Name: "About", SubnavItems: []string{SubnavItems[NvMain]}}, + "profile": {Key: "profile", Slug: "/admin/profile{trailingslash:\\/?}", Name: "Profile", SubnavItems: []string{SubnavItems[NvMain]}}, + "add-event-source": {Key: "add-event-source", Slug: "/admin/add-event-source{trailingslash:\\/?}", Name: "Add Event Source", SubnavItems: []string{SubnavItems[NvMain]}}, + "settings": {Key: "settings", Slug: "/admin/profile/settings{trailingslash:\\/?}", Name: "Settings", SubnavItems: []string{SubnavItems[NvMain]}}, + "map-embed": {Key: "map-embed", Slug: "/map-embed{trailingslash:\\/?}", Name: "MapEmbed", SubnavItems: []string{SubnavItems[NvMain]}}, + "event-detail": {Key: "event-detail", Slug: "/event/{" + EVENT_ID_KEY + "}{trailingslash:\\/?}", Name: "Event Detail", SubnavItems: []string{SubnavItems[NvMain], SubnavItems[NvCart]}}, + "add-event": {Key: "add-event", Slug: "/admin/event/new{trailingslash:\\/?}", Name: "Add Event", SubnavItems: []string{SubnavItems[NvMain]}}, + "edit-event": {Key: "edit-event", Slug: "/admin/event/{" + EVENT_ID_KEY + "}/edit{trailingslash:\\/?}", Name: "Edit Event", SubnavItems: []string{SubnavItems[NvMain]}}, + "attendees-event": {Key: "attendees-event", Slug: "/admin/event/{" + EVENT_ID_KEY + "}/attendees{trailingslash:\\/?}", Name: "Event Attendees", SubnavItems: []string{SubnavItems[NvMain]}}, } type Subcategory struct { diff --git a/functions/gateway/helpers/utils_test.go b/functions/gateway/helpers/utils_test.go index d4525e2f..066cf615 100644 --- a/functions/gateway/helpers/utils_test.go +++ b/functions/gateway/helpers/utils_test.go @@ -187,8 +187,8 @@ func TestSetCloudFlareKV(t *testing.T) { mockCloudflareServer.Start() defer mockCloudflareServer.Close() - boundCfAddress := mockCloudflareServer.Listener.Addr().String() - os.Setenv("ZITADEL_INSTANCE_HOST", boundCfAddress) + boundCfAddress := fmt.Sprintf("http://%s", mockCloudflareServer.Listener.Addr().String()) + os.Setenv("CLOUDFLARE_API_BASE_URL", boundCfAddress) zitadelListener, err := test_helpers.BindToPort(t, zitadelEndpoint) if err != nil { diff --git a/functions/gateway/main.go b/functions/gateway/main.go index 02970343..da2697e7 100644 --- a/functions/gateway/main.go +++ b/functions/gateway/main.go @@ -62,7 +62,7 @@ func init() { // from `Require` which always creates a new session if the user isn't logged in... // the complexity is we might want "in the middle", which would be "auto-refresh // the session, but DO NOT redirect to /login if the user's session is expired'" - // session duration might be a Zitadel configuration issue + // session duration might be a ZFvitadel configuration issue {helpers.SitePages["event-detail"].Slug, "GET", handlers.GetEventDetailsPage, Check}, // API routes @@ -94,35 +94,19 @@ func init() { {"/api/purchasables/{event_id:[0-9a-fA-F-]+}", "PUT", dynamodb_handlers.UpdatePurchasableHandler, Require}, // Update an existing purchasable {"/api/purchasables/{event_id:[0-9a-fA-F-]+}", "DELETE", dynamodb_handlers.DeletePurchasableHandler, Require}, // Delete a purchasable - // // Event RSVPs routes - {"/api/event-rsvps/{event_id:[0-9a-fA-F-]+}/{user_id:[0-9a-fA-F-]+}", "POST", dynamodb_handlers.CreateEventRsvpHandler, None}, // Create a new event RSVP - {"/api/event-rsvps/{event_id:[0-9a-fA-F-]+}/{user_id:[0-9a-fA-F-]+}", "GET", dynamodb_handlers.GetEventRsvpByPkHandler, Require}, // Get a specific event RSVP - {"/api/event-rsvps/event/{event_id:[0-9a-fA-F-]+}", "GET", dynamodb_handlers.GetEventRsvpsByEventIDHandler, Require}, // Get all event RSVPs - {"/api/event-rsvps/user/{user_id:[0-9a-fA-F-]+}", "GET", dynamodb_handlers.GetEventRsvpsByUserIDHandler, Require}, // Get a specific event RSVP - {"/api/event-rsvps/{event_id:[0-9a-fA-F-]+}/{user_id:[0-9a-fA-F-]+}", "PUT", dynamodb_handlers.UpdateEventRsvpHandler, Require}, // Update an existing event RSVP - {"/api/event-rsvps/{event_id:[0-9a-fA-F-]+}/{user_id:[0-9a-fA-F-]+}", "DELETE", dynamodb_handlers.DeleteEventRsvpHandler, Require}, // Delete an event RSVP - - // Registrations - {"/api/registrations/{event_id:[0-9a-fA-F-]+}/{user_id:(?:anonymous|[0-9a-fA-F-]+)}", "POST", dynamodb_handlers.CreateRegistrationHandler, None}, // Create a new Registration - {"/api/registrations/{event_id:[0-9a-fA-F-]+}/{user_id:[0-9a-fA-F-]+}", "GET", dynamodb_handlers.GetRegistrationByPkHandler, Require}, // Get a registration by primary key - {"/api/registrations/user/{user_id:[0-9a-fA-F-]+}", "GET", dynamodb_handlers.GetRegistrationsByUserIDHandler, Require}, // Get a specific event RSVP - {"/api/registrations/event/{event_id:[0-9a-fA-F-]+}", "GET", dynamodb_handlers.GetRegistrationsByEventIDHandler, Require}, // Get all event RSVPs - {"/api/registrations/{event_id:[0-9a-fA-F-]+}/{user_id:[0-9a-fA-F-]+}", "PUT", dynamodb_handlers.UpdateRegistrationHandler, Require}, // Update an existing Registration - {"/api/registrations/{event_id:[0-9a-fA-F-]+}/{user_id:[0-9a-fA-F-]+}", "DELETE", dynamodb_handlers.DeleteRegistrationHandler, Require}, // Delete an event RSVP - // RegistrationFields - {"/api/registration-fields/{event_id:[0-9a-fA-F-]+}", "POST", dynamodb_handlers.CreateRegistrationFieldsHandler, Require}, // Create a new - {"/api/registration-fields/{event_id:[0-9a-fA-F-]+}", "GET", dynamodb_handlers.GetRegistrationFieldsByEventIDHandler, None}, // Get all - {"/api/registration-fields/{event_id:[0-9a-fA-F-]+}", "PUT", dynamodb_handlers.UpdateRegistrationFieldsHandler, Require}, // Update an existing - {"/api/registration-fields/{event_id:[0-9a-fA-F-]+}", "DELETE", dynamodb_handlers.DeleteRegistrationFieldsHandler, Require}, // Delete an + {"/api/registration-fields/{event_id:[0-9a-fA-F-]+}", "POST", dynamodb_handlers.CreateRegistrationFieldsHandler, Require}, + {"/api/registration-fields/{event_id:[0-9a-fA-F-]+}", "GET", dynamodb_handlers.GetRegistrationFieldsByEventIDHandler, None}, + {"/api/registration-fields/{event_id:[0-9a-fA-F-]+}", "PUT", dynamodb_handlers.UpdateRegistrationFieldsHandler, Require}, + {"/api/registration-fields/{event_id:[0-9a-fA-F-]+}", "DELETE", dynamodb_handlers.DeleteRegistrationFieldsHandler, Require}, // Purchases - {"/api/purchases/{event_id:[0-9a-fA-F-]+}/{user_id:[0-9a-fA-F-]+}", "POST", dynamodb_handlers.CreatePurchaseHandler, Require}, // Create a new event RSVP - {"/api/purchases/{event_id:[0-9a-fA-F-]+}/{user_id:[0-9a-fA-F-]+}/{created_at:[0-9]+}", "GET", dynamodb_handlers.GetPurchaseByPkHandler, Require}, // Get a specific event RSVP - {"/api/purchases/event/{event_id:[0-9a-fA-F-]+}", "GET", dynamodb_handlers.GetPurchasesByEventIDHandler, Require}, // Get all event RSVPs - {"/api/purchases/user/{user_id:[0-9a-fA-F-]+}", "GET", dynamodb_handlers.GetPurchasesByUserIDHandler, Require}, // Get a specific event RSVP - {"/api/purchases/{event_id:[0-9a-fA-F-]+}/{user_id:[0-9a-fA-F-]+}/{created_at:[0-9]+}", "PUT", dynamodb_handlers.UpdatePurchaseHandler, None}, // Update an existing event RSVP - {"/api/purchases/{event_id:[0-9a-fA-F-]+}/{user_id:[0-9a-fA-F-]+}", "DELETE", dynamodb_handlers.DeletePurchaseHandler, None}, // Delete an event RSVP + {"/api/purchases/{event_id:[0-9a-fA-F-]+}/{user_id:[0-9a-fA-F-]+}", "POST", dynamodb_handlers.CreatePurchaseHandler, Require}, // Create a new event Purchase + {"/api/purchases/{event_id:[0-9a-fA-F-]+}/{user_id:[0-9a-fA-F-]+}/{created_at:[0-9]+}", "GET", dynamodb_handlers.GetPurchaseByPkHandler, Require}, // Get a specific event Purchase + {"/api/purchases/event/{event_id:[0-9a-fA-F-]+}", "GET", dynamodb_handlers.GetPurchasesByEventIDHandler, Require}, // Get all event Purchases + {"/api/purchases/user/{user_id:[0-9a-fA-F-]+}", "GET", dynamodb_handlers.GetPurchasesByUserIDHandler, Require}, // Get a specific event Purchase + {"/api/purchases/{event_id:[0-9a-fA-F-]+}/{user_id:[0-9a-fA-F-]+}/{created_at:[0-9]+}", "PUT", dynamodb_handlers.UpdatePurchaseHandler, None}, // Update an existing event Purchase + {"/api/purchases/{event_id:[0-9a-fA-F-]+}/{user_id:[0-9a-fA-F-]+}", "DELETE", dynamodb_handlers.DeletePurchaseHandler, None}, // Delete an event Purchase // Checkout Session {"/api/checkout/{event_id:[0-9a-fA-F-]+}", "POST", handlers.CreateCheckoutSessionHandler, Check}, diff --git a/functions/gateway/services/dynamodb_service/event_rsvp_service.go b/functions/gateway/services/dynamodb_service/event_rsvp_service.go deleted file mode 100644 index bfc5e3d2..00000000 --- a/functions/gateway/services/dynamodb_service/event_rsvp_service.go +++ /dev/null @@ -1,264 +0,0 @@ -// TODO: change all fmt to log printout in new rds handlers and services -package dynamodb_service - -import ( - "context" - "fmt" - "log" - "strconv" - "time" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" - "github.com/aws/aws-sdk-go-v2/service/dynamodb" - dynamodb_types "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/meetnearme/api/functions/gateway/helpers" - internal_types "github.com/meetnearme/api/functions/gateway/types" -) - -var rsvpTableName = helpers.GetDbTableName(helpers.RsvpsTablePrefix) - -func init () { - rsvpTableName = helpers.GetDbTableName(helpers.RsvpsTablePrefix) -} - -type EventRsvpService struct{} - -func NewEventRsvpService() internal_types.EventRsvpServiceInterface { - return &EventRsvpService{} -} - -func (s *EventRsvpService) InsertEventRsvp(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventRsvp internal_types.EventRsvpInsert) (*internal_types.EventRsvp, error) { - // Validate the eventRsvp object - if err := validate.Struct(eventRsvp); err != nil { - return nil, fmt.Errorf("validation failed: %w", err) - } - - item, err := attributevalue.MarshalMap(&eventRsvp) - if err != nil { - return nil, err - } - - if (rsvpTableName == "") { - return nil, fmt.Errorf("ERR: rsvpTableName is empty") - } - - input := &dynamodb.PutItemInput{ - Item: item, - TableName: aws.String(registrationTableName), - ConditionExpression: aws.String("attribute_not_exists(eventId) AND attribute_not_exists(userId)"), - } - - - res, err := dynamodbClient.PutItem(ctx, input) - if err != nil { - log.Print("htting error in put item dynamo") - return nil, err - } - - var insertedRegistration internal_types.EventRsvp - - err = attributevalue.UnmarshalMap(res.Attributes, &insertedRegistration) - if err != nil { - return nil, err - } - - // return registration, nil - return &insertedRegistration, nil -} - - -func (s *EventRsvpService) GetEventRsvpByPk(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId string) (*internal_types.EventRsvp, error) { - input := &dynamodb.GetItemInput{ - TableName: aws.String(registrationTableName), - Key: map[string]dynamodb_types.AttributeValue{ - "eventId": &dynamodb_types.AttributeValueMemberS{Value: eventId}, - "userId": &dynamodb_types.AttributeValueMemberS{Value: userId}, - }, - } - - result, err := dynamodbClient.GetItem(ctx, input) - if err != nil { - return nil, err - } - - var eventRsvp internal_types.EventRsvp - err = attributevalue.UnmarshalMap(result.Item, &eventRsvp) - if err != nil { - return nil, err - } - - return &eventRsvp, nil -} - -func (s *EventRsvpService) GetEventRsvpsByEventID(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId string) ([]internal_types.EventRsvp, error) { - queryInput := &dynamodb.QueryInput{ - TableName: aws.String(registrationTableName), - KeyConditions: map[string]dynamodb_types.Condition{ - "eventId": { - ComparisonOperator: dynamodb_types.ComparisonOperatorEq, - AttributeValueList: []dynamodb_types.AttributeValue{ - &dynamodb_types.AttributeValueMemberS{Value: eventId}, - }, - }, - }, - } - - // Run the query with the constructed QueryInput - result, err := dynamodbClient.Query(ctx, queryInput) - if err != nil { - return nil, err - } - - var eventRsvps []internal_types.EventRsvp - err = attributevalue.UnmarshalListOfMaps(result.Items, &eventRsvps) - if err != nil { - return nil, err - } - - return eventRsvps, nil -} - -func (s *EventRsvpService) GetEventRsvpsByUserID(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, userId string) ([]internal_types.EventRsvp, error) { - input := &dynamodb.QueryInput{ - TableName: aws.String(registrationTableName), - IndexName: aws.String("userIdGsi"), // GSI name - KeyConditionExpression: aws.String("userId = :userId"), - ExpressionAttributeValues: map[string]dynamodb_types.AttributeValue{ - ":userId": &dynamodb_types.AttributeValueMemberS{Value: userId}, - }, - } - - result, err := dynamodbClient.Query(context.TODO(), input) - if err != nil { - log.Fatalf("Query GSI failed, %v", err) - } - log.Printf("query gsi: %v", result) - - inputScan := &dynamodb.ScanInput{ - TableName: aws.String(registrationTableName), - IndexName: aws.String("userIdGsi"), // Scan the GSI - } - - resultScan, err := dynamodbClient.Scan(ctx, inputScan) - if err != nil { - log.Fatalf("Scan GSI failed: %v", err) - } - - log.Printf("GSI scan result: %v", resultScan.Items) - - var eventRsvps []internal_types.EventRsvp - err = attributevalue.UnmarshalListOfMaps(result.Items, &eventRsvps) - if err != nil { - return nil, err - } - - return eventRsvps, nil -} - -func (s *EventRsvpService) UpdateEventRsvp(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId string, eventRsvp internal_types.EventRsvpUpdate) (*internal_types.EventRsvp, error) { - if rsvpTableName == "" { - return nil, fmt.Errorf("ERR: rsvpTableName is empty") - } - input := &dynamodb.UpdateItemInput{ - TableName: aws.String(rsvpTableName), - Key: map[string]dynamodb_types.AttributeValue{ - "eventId": &dynamodb_types.AttributeValueMemberS{Value: eventId}, - "userId": &dynamodb_types.AttributeValueMemberS{Value: userId}, - }, - ExpressionAttributeNames: make(map[string]string), - ExpressionAttributeValues: make(map[string]dynamodb_types.AttributeValue), - UpdateExpression: aws.String("SET"), - ReturnValues: dynamodb_types.ReturnValueAllNew, - } - - if eventRsvp.EventSourceID != "" { - input.ExpressionAttributeNames["#eventSourceId"] = "eventSourceId" - input.ExpressionAttributeValues[":eventSourceId"] = &dynamodb_types.AttributeValueMemberS{Value: eventRsvp.EventSourceID} - *input.UpdateExpression += " #eventSourceId = :eventSourceId," - } - - if eventRsvp.EventSourceType != "" { - input.ExpressionAttributeNames["#eventSourceType"] = "eventSourceType" - input.ExpressionAttributeValues[":eventSourceType"] = &dynamodb_types.AttributeValueMemberS{Value: eventRsvp.EventSourceType} - *input.UpdateExpression += " #eventSourceType = :eventSourceType," - } - - if eventRsvp.Status != "" { - input.ExpressionAttributeNames["#status"] = "status" - input.ExpressionAttributeValues[":status"] = &dynamodb_types.AttributeValueMemberS{Value: eventRsvp.Status} - *input.UpdateExpression += " #status = :status," - } - - // Set the updatedAt field - currentTime := time.Now().Unix() - input.ExpressionAttributeNames["#updatedAt"] = "updatedAt" - input.ExpressionAttributeValues[":updatedAt"] = &dynamodb_types.AttributeValueMemberN{Value: strconv.FormatInt(currentTime, 10)} - *input.UpdateExpression += "#updatedAt = :updatedAt" - - // Execute the update - res, err := dynamodbClient.UpdateItem(ctx, input) - if err != nil { - return nil, err - } - - // Unmarshal the updated registration - var updatedEventRsvp internal_types.EventRsvp - err = attributevalue.UnmarshalMap(res.Attributes, &updatedEventRsvp) - if err != nil { - return nil, err - } - - return &updatedEventRsvp, nil -} - -func (s *EventRsvpService) DeleteEventRsvp(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId string) error { - input := &dynamodb.DeleteItemInput{ - TableName: aws.String(registrationTableName), - Key: map[string]dynamodb_types.AttributeValue{ - "eventId": &dynamodb_types.AttributeValueMemberS{Value: eventId}, - "userId": &dynamodb_types.AttributeValueMemberS{Value: userId}, - }, - } - - _, err := dynamodbClient.DeleteItem(ctx, input) - if err != nil { - return err - } - - log.Printf("registration fields successfully deleted") - return nil -} - -type MockEventRsvpService struct { - InsertEventRsvpFunc func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventRsvp internal_types.EventRsvpInsert) (*internal_types.EventRsvp, error) - GetEventRsvpByPkFunc func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId string) (*internal_types.EventRsvp, error) - GetEventRsvpsByUserIDFunc func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, userID string) ([]internal_types.EventRsvp, error) // New function - GetEventRsvpsByEventIDFunc func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventID string) ([]internal_types.EventRsvp, error) // New function - UpdateEventRsvpFunc func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId string, eventRsvp internal_types.EventRsvpUpdate) (*internal_types.EventRsvp, error) - DeleteEventRsvpFunc func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId string) error -} - -func (m *MockEventRsvpService) InsertEventRsvp(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventRsvp internal_types.EventRsvpInsert) (*internal_types.EventRsvp, error) { - return m.InsertEventRsvpFunc(ctx, dynamodbClient, eventRsvp) -} - -func (m *MockEventRsvpService) GetEventRsvpByPk(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId string) (*internal_types.EventRsvp, error) { - return m.GetEventRsvpByPkFunc(ctx, dynamodbClient, eventId, userId) -} - -func (m *MockEventRsvpService) UpdateEventRsvp(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId string, eventRsvp internal_types.EventRsvpUpdate) (*internal_types.EventRsvp, error) { - return m.UpdateEventRsvpFunc(ctx, dynamodbClient, eventId, userId, eventRsvp) -} - -func (m *MockEventRsvpService) DeleteEventRsvp(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId string) error { - return m.DeleteEventRsvpFunc(ctx, dynamodbClient, eventId, userId) -} - -func (m *MockEventRsvpService) GetEventRsvpsByUserID(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, userID string) ([]internal_types.EventRsvp, error) { - return m.GetEventRsvpsByUserIDFunc(ctx, dynamodbClient, userID) -} - -func (m *MockEventRsvpService) GetEventRsvpsByEventID(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventID string) ([]internal_types.EventRsvp, error) { - return m.GetEventRsvpsByEventIDFunc(ctx, dynamodbClient, eventID) -} diff --git a/functions/gateway/services/dynamodb_service/purchases_service.go b/functions/gateway/services/dynamodb_service/purchases_service.go index f085da1e..b1d0507b 100644 --- a/functions/gateway/services/dynamodb_service/purchases_service.go +++ b/functions/gateway/services/dynamodb_service/purchases_service.go @@ -54,15 +54,14 @@ func (s *PurchaseService) InsertPurchase(ctx context.Context, dynamodbClient int ConditionExpression: aws.String("attribute_not_exists(compositeKey)"), } - res, err := dynamodbClient.PutItem(ctx, input) + _, err = dynamodbClient.PutItem(ctx, input) if err != nil { - log.Print("hitting error in put item dynamo") + log.Print("error inserting item in database") return nil, err } var insertedPurchase internal_types.Purchase - - err = attributevalue.UnmarshalMap(res.Attributes, &insertedPurchase) + err = attributevalue.UnmarshalMap(item, &insertedPurchase) if err != nil { return nil, err } diff --git a/functions/gateway/services/dynamodb_service/registration_service.go b/functions/gateway/services/dynamodb_service/registration_service.go deleted file mode 100644 index e6a2cb66..00000000 --- a/functions/gateway/services/dynamodb_service/registration_service.go +++ /dev/null @@ -1,272 +0,0 @@ -package dynamodb_service - -import ( - "context" - "fmt" - "log" - "strconv" - "strings" - "time" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" - "github.com/aws/aws-sdk-go-v2/service/dynamodb" - dynamodb_types "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/meetnearme/api/functions/gateway/helpers" - internal_types "github.com/meetnearme/api/functions/gateway/types" -) - -var registrationTableName = helpers.GetDbTableName(helpers.RegistrationsTablePrefix) - -func init() { - registrationTableName = helpers.GetDbTableName(helpers.RegistrationsTablePrefix) -} - -// RegistrationService is the concrete implementation of the RegistrationServiceInterface. -type RegistrationService struct{} - -func NewRegistrationService() internal_types.RegistrationServiceInterface { - return &RegistrationService{} -} - -func (s *RegistrationService) InsertRegistration(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, registration internal_types.RegistrationInsert, eventId, userId string) (*internal_types.Registration, error) { - // Validate the registration object - if err := validate.Struct(registration); err != nil { - return nil, fmt.Errorf("validation failed: %w", err) - } - - // Handle anonymous users by prefixing with unix timestamp - if userId == "anonymous" { - userId = fmt.Sprintf("%d-anonymous", time.Now().Unix()) - } - - // Update the userId in the registration object before marshaling - registration.UserId = userId - - if registration.CreatedAt.IsZero() { - registration.CreatedAt = time.Now() - } - - item, err := attributevalue.MarshalMap(®istration) - if err != nil { - return nil, err - } - - if registrationTableName == "" { - return nil, fmt.Errorf("ERR: registrationTableName is empty") - } - - input := &dynamodb.PutItemInput{ - Item: item, - TableName: aws.String(registrationTableName), - ConditionExpression: aws.String("attribute_not_exists(eventId) AND attribute_not_exists(userId)"), - } - - res, err := dynamodbClient.PutItem(ctx, input) - if err != nil { - log.Print("htting error in put item dynamo") - return nil, err - } - - var insertedRegistration internal_types.Registration - - err = attributevalue.UnmarshalMap(res.Attributes, &insertedRegistration) - if err != nil { - return nil, err - } - - return &insertedRegistration, nil -} - -func (s *RegistrationService) GetRegistrationByPk(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId string) (*internal_types.Registration, error) { - input := &dynamodb.GetItemInput{ - TableName: aws.String(registrationTableName), - Key: map[string]dynamodb_types.AttributeValue{ - "eventId": &dynamodb_types.AttributeValueMemberS{Value: eventId}, - "userId": &dynamodb_types.AttributeValueMemberS{Value: userId}, - }, - } - - result, err := dynamodbClient.GetItem(ctx, input) - if err != nil { - return nil, err - } - - var registration internal_types.Registration - err = attributevalue.UnmarshalMap(result.Item, ®istration) - if err != nil { - return nil, err - } - - return ®istration, nil -} - -func (s *RegistrationService) GetRegistrationsByEventID(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId string, limit int32, startKey string) ([]internal_types.Registration, map[string]dynamodb_types.AttributeValue, error) { - queryInput := &dynamodb.QueryInput{ - TableName: aws.String(registrationTableName), - Limit: aws.Int32(limit), - KeyConditions: map[string]dynamodb_types.Condition{ - "eventId": { - ComparisonOperator: dynamodb_types.ComparisonOperatorEq, - AttributeValueList: []dynamodb_types.AttributeValue{ - &dynamodb_types.AttributeValueMemberS{Value: eventId}, - }, - }, - }, - } - - // If startKey is provided, use it for pagination - if startKey != "" { - // Extract createdAtString from the composite key (value after second '_') - parts := strings.Split(startKey, "_") - if len(parts) != 2 { - return nil, nil, fmt.Errorf("invalid startKey format") - } - userId := parts[0] - eventId := parts[1] - - queryInput.ExclusiveStartKey = map[string]dynamodb_types.AttributeValue{ - "userId": &dynamodb_types.AttributeValueMemberS{Value: userId}, - "eventId": &dynamodb_types.AttributeValueMemberS{Value: eventId}, - } - } - - result, err := dynamodbClient.Query(ctx, queryInput) - if err != nil { - return nil, nil, err - } - - var registrations []internal_types.Registration - err = attributevalue.UnmarshalListOfMaps(result.Items, ®istrations) - if err != nil { - return nil, nil, err - } - - return registrations, result.LastEvaluatedKey, nil -} - -func (s *RegistrationService) GetRegistrationsByUserID(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, userId string) ([]internal_types.Registration, error) { - input := &dynamodb.QueryInput{ - TableName: aws.String(registrationTableName), - IndexName: aws.String("userIdGsi"), // GSI name - KeyConditionExpression: aws.String("userId = :userId"), - ExpressionAttributeValues: map[string]dynamodb_types.AttributeValue{ - ":userId": &dynamodb_types.AttributeValueMemberS{Value: userId}, - }, - } - - result, err := dynamodbClient.Query(context.TODO(), input) - if err != nil { - log.Fatalf("Query GSI failed, %v", err) - } - - var registrations []internal_types.Registration - err = attributevalue.UnmarshalListOfMaps(result.Items, ®istrations) - if err != nil { - return nil, err - } - - return registrations, nil -} - -func (s *RegistrationService) UpdateRegistration(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId string, registration internal_types.RegistrationUpdate) (*internal_types.Registration, error) { - if registrationTableName == "" { - return nil, fmt.Errorf("ERR: registrationTableName is empty") - } - - // Build the UpdateItemInput with composite key - input := &dynamodb.UpdateItemInput{ - TableName: aws.String(registrationTableName), - Key: map[string]dynamodb_types.AttributeValue{ - "eventId": &dynamodb_types.AttributeValueMemberS{Value: eventId}, - "userId": &dynamodb_types.AttributeValueMemberS{Value: userId}, - }, - ExpressionAttributeNames: make(map[string]string), - ExpressionAttributeValues: make(map[string]dynamodb_types.AttributeValue), - UpdateExpression: aws.String("SET"), - ReturnValues: dynamodb_types.ReturnValueAllNew, - } - - // Check if responses need to be updated - if len(registration.Responses) > 0 { - input.ExpressionAttributeNames["#responses"] = "responses" - responses, err := attributevalue.MarshalList(registration.Responses) - if err != nil { - return nil, err - } - input.ExpressionAttributeValues[":responses"] = &dynamodb_types.AttributeValueMemberL{Value: responses} - *input.UpdateExpression += " #responses = :responses," - } - - // Set the updatedAt field - currentTime := time.Now().Unix() - input.ExpressionAttributeNames["#updatedAt"] = "updatedAt" - input.ExpressionAttributeValues[":updatedAt"] = &dynamodb_types.AttributeValueMemberN{Value: strconv.FormatInt(currentTime, 10)} - *input.UpdateExpression += " #updatedAt = :updatedAt" - - // Execute the update - res, err := dynamodbClient.UpdateItem(ctx, input) - if err != nil { - return nil, err - } - - // Unmarshal the updated registration - var updatedRegistration internal_types.Registration - err = attributevalue.UnmarshalMap(res.Attributes, &updatedRegistration) - if err != nil { - return nil, err - } - - return &updatedRegistration, nil -} - -func (s *RegistrationService) DeleteRegistration(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId string) error { - input := &dynamodb.DeleteItemInput{ - TableName: aws.String(registrationTableName), - Key: map[string]dynamodb_types.AttributeValue{ - "eventId": &dynamodb_types.AttributeValueMemberS{Value: eventId}, - "userId": &dynamodb_types.AttributeValueMemberS{Value: userId}, - }, - } - - _, err := dynamodbClient.DeleteItem(ctx, input) - if err != nil { - return err - } - - log.Printf("registration fields successfully deleted") - return nil -} - -type MockRegistrationService struct { - InsertRegistrationFunc func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, registration internal_types.RegistrationInsert, eventId, userId string) (*internal_types.Registration, error) - GetRegistrationByPkFunc func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId string) (*internal_types.Registration, error) - GetRegistrationsByEventIDFunc func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId string, limit int32, startKey string) ([]internal_types.Registration, map[string]dynamodb_types.AttributeValue, error) - GetRegistrationsByUserIDFunc func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, userId string) ([]internal_types.Registration, error) - UpdateRegistrationFunc func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId string, registration internal_types.RegistrationUpdate) (*internal_types.Registration, error) - DeleteRegistrationFunc func(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId string) error -} - -func (m *MockRegistrationService) InsertRegistration(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, registration internal_types.RegistrationInsert, eventId, userId string) (*internal_types.Registration, error) { - return m.InsertRegistrationFunc(ctx, dynamodbClient, registration, eventId, userId) -} - -func (m *MockRegistrationService) GetRegistrationByPk(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId string) (*internal_types.Registration, error) { - return m.GetRegistrationByPkFunc(ctx, dynamodbClient, eventId, userId) -} - -func (m *MockRegistrationService) GetRegistrationsByEventID(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId string, limit int32, startKey string) ([]internal_types.Registration, map[string]dynamodb_types.AttributeValue, error) { - return m.GetRegistrationsByEventIDFunc(ctx, dynamodbClient, eventId, limit, startKey) -} - -func (m *MockRegistrationService) GetRegistrationsByUserID(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, userId string) ([]internal_types.Registration, error) { - return m.GetRegistrationsByUserIDFunc(ctx, dynamodbClient, userId) -} - -func (m *MockRegistrationService) UpdateRegistration(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId string, registration internal_types.RegistrationUpdate) (*internal_types.Registration, error) { - return m.UpdateRegistrationFunc(ctx, dynamodbClient, eventId, userId, registration) -} - -func (m *MockRegistrationService) DeleteRegistration(ctx context.Context, dynamodbClient internal_types.DynamoDBAPI, eventId, userId string) error { - return m.DeleteRegistrationFunc(ctx, dynamodbClient, eventId, userId) -} diff --git a/functions/gateway/services/dynamodb_service/registration_service_test.go b/functions/gateway/services/dynamodb_service/registration_service_test.go deleted file mode 100644 index 1a489e78..00000000 --- a/functions/gateway/services/dynamodb_service/registration_service_test.go +++ /dev/null @@ -1,231 +0,0 @@ -package dynamodb_service - -import ( - "context" - "testing" - "time" - - "github.com/aws/aws-sdk-go-v2/service/dynamodb" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/meetnearme/api/functions/gateway/test_helpers" - internal_types "github.com/meetnearme/api/functions/gateway/types" -) - -func TestInsertRegistration(t *testing.T) { - mockDynamoDBClient := &test_helpers.MockDynamoDBClient{ - PutItemFunc: func(ctx context.Context, input *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) { - return &dynamodb.PutItemOutput{}, nil - }, - } - service := NewRegistrationService() - - now := time.Now() - registration := internal_types.RegistrationInsert{ - EventId: "eventId", - UserId: "userId", - Responses: []map[string]interface{}{ - {"question1": "answer1"}, - }, - CreatedAt: now, - UpdatedAt: now, - } - - result, err := service.InsertRegistration(context.TODO(), mockDynamoDBClient, registration, registration.EventId, registration.UserId) - - if err != nil { - t.Errorf("expected no error, got %v", err) - } - if result == nil { - t.Error("expected non-nil result") - } -} - -func TestGetRegistrationByPk(t *testing.T) { - mockDynamoDBClient := &test_helpers.MockDynamoDBClient{ - GetItemFunc: func(ctx context.Context, input *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { - return &dynamodb.GetItemOutput{ - Item: map[string]types.AttributeValue{ - "eventId": &types.AttributeValueMemberS{Value: "eventId"}, - "userId": &types.AttributeValueMemberS{Value: "userId"}, - "responses": &types.AttributeValueMemberL{ - Value: []types.AttributeValue{ - &types.AttributeValueMemberM{ - Value: map[string]types.AttributeValue{ - "question1": &types.AttributeValueMemberS{Value: "answer1"}, - }, - }, - }, - }, - "createdAt": &types.AttributeValueMemberS{Value: time.Now().Format(time.RFC3339)}, - "updatedAt": &types.AttributeValueMemberS{Value: time.Now().Format(time.RFC3339)}, - }, - }, nil - }, - } - service := NewRegistrationService() - - result, err := service.GetRegistrationByPk(context.TODO(), mockDynamoDBClient, "eventId", "userId") - - if err != nil { - t.Errorf("expected no error, got %v", err) - } - if result == nil { - t.Error("expected non-nil result") - } - // Validate returned fields - if result.EventId != "eventId" || result.UserId != "userId" || len(result.Responses) == 0 { - t.Errorf("unexpected result: %+v", result) - } -} -func TestGetRegistrationsByEventID(t *testing.T) { - t.Run("basic query", func(t *testing.T) { - mockDynamoDBClient := &test_helpers.MockDynamoDBClient{ - QueryFunc: func(ctx context.Context, input *dynamodb.QueryInput, optFns ...func(*dynamodb.Options)) (*dynamodb.QueryOutput, error) { - return &dynamodb.QueryOutput{ - Items: []map[string]types.AttributeValue{ - { - "eventId": &types.AttributeValueMemberS{Value: "eventId"}, - "userId": &types.AttributeValueMemberS{Value: "userId"}, - "responses": &types.AttributeValueMemberL{ - Value: []types.AttributeValue{ - &types.AttributeValueMemberM{ - Value: map[string]types.AttributeValue{ - "question1": &types.AttributeValueMemberS{Value: "answer1"}, - }, - }, - }, - }, - "createdAt": &types.AttributeValueMemberS{Value: time.Now().Format(time.RFC3339)}, - "updatedAt": &types.AttributeValueMemberS{Value: time.Now().Format(time.RFC3339)}, - }, - }, - }, nil - }, - } - service := NewRegistrationService() - - results, _, err := service.GetRegistrationsByEventID(context.TODO(), mockDynamoDBClient, "eventId", 100, "") - - if err != nil { - t.Errorf("expected no error, got %v", err) - } - if len(results) == 0 { - t.Error("expected non-empty results") - } - }) - - t.Run("with pagination", func(t *testing.T) { - mockDynamoDBClient := &test_helpers.MockDynamoDBClient{ - QueryFunc: func(ctx context.Context, input *dynamodb.QueryInput, optFns ...func(*dynamodb.Options)) (*dynamodb.QueryOutput, error) { - // Verify the ExclusiveStartKey was set correctly - if input.ExclusiveStartKey != nil { - expectedUserId := "user123" - expectedEventId := "event456" - gotUserId := input.ExclusiveStartKey["userId"].(*types.AttributeValueMemberS).Value - gotEventId := input.ExclusiveStartKey["eventId"].(*types.AttributeValueMemberS).Value - - if gotUserId != expectedUserId || gotEventId != expectedEventId { - t.Errorf("incorrect ExclusiveStartKey values, got userId=%s, eventId=%s, want userId=%s, eventId=%s", - gotUserId, gotEventId, expectedUserId, expectedEventId) - } - } - - return &dynamodb.QueryOutput{ - Items: []map[string]types.AttributeValue{ - { - "eventId": &types.AttributeValueMemberS{Value: "event456"}, - "userId": &types.AttributeValueMemberS{Value: "nextUser"}, - "responses": &types.AttributeValueMemberL{ - Value: []types.AttributeValue{ - &types.AttributeValueMemberM{ - Value: map[string]types.AttributeValue{ - "question1": &types.AttributeValueMemberS{Value: "answer1"}, - }, - }, - }, - }, - "createdAt": &types.AttributeValueMemberS{Value: time.Now().Format(time.RFC3339)}, - "updatedAt": &types.AttributeValueMemberS{Value: time.Now().Format(time.RFC3339)}, - }, - }, - LastEvaluatedKey: map[string]types.AttributeValue{ - "eventId": &types.AttributeValueMemberS{Value: "event456"}, - "userId": &types.AttributeValueMemberS{Value: "nextUser"}, - }, - }, nil - }, - } - service := NewRegistrationService() - - startKey := "user123_event456" - results, lastKey, err := service.GetRegistrationsByEventID(context.TODO(), mockDynamoDBClient, "event456", 100, startKey) - - if err != nil { - t.Errorf("expected no error, got %v", err) - } - if len(results) == 0 { - t.Error("expected non-empty results") - } - if lastKey == nil { - t.Error("expected LastEvaluatedKey to be present") - } - }) -} - -func TestUpdateRegistration(t *testing.T) { - mockDynamoDBClient := &test_helpers.MockDynamoDBClient{ - UpdateItemFunc: func(ctx context.Context, input *dynamodb.UpdateItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.UpdateItemOutput, error) { - return &dynamodb.UpdateItemOutput{ - Attributes: map[string]types.AttributeValue{ - "eventId": &types.AttributeValueMemberS{Value: "eventId"}, - "userId": &types.AttributeValueMemberS{Value: "userId"}, - "responses": &types.AttributeValueMemberL{ - Value: []types.AttributeValue{ - &types.AttributeValueMemberM{ - Value: map[string]types.AttributeValue{ - "question1": &types.AttributeValueMemberS{Value: "updatedAnswer"}, - }, - }, - }, - }, - "createdAt": &types.AttributeValueMemberS{Value: time.Now().Format(time.RFC3339)}, - "updatedAt": &types.AttributeValueMemberS{Value: time.Now().Format(time.RFC3339)}, - }, - }, nil - }, - } - service := NewRegistrationService() - - updatedRegistration, err := service.UpdateRegistration(context.TODO(), mockDynamoDBClient, "eventId", "userId", internal_types.RegistrationUpdate{ - Responses: []map[string]interface{}{ - {"question1": "updatedAnswer"}, - }, - UpdatedAt: time.Now(), - }) - - if err != nil { - t.Errorf("expected no error, got %v", err) - } - if updatedRegistration == nil { - t.Error("expected non-nil result") - } - // Validate returned fields - if updatedRegistration.UserId != "userId" || len(updatedRegistration.Responses) == 0 { - t.Errorf("unexpected result: %+v", updatedRegistration) - } -} - -func TestDeleteRegistration(t *testing.T) { - mockDynamoDBClient := &test_helpers.MockDynamoDBClient{ - DeleteItemFunc: func(ctx context.Context, input *dynamodb.DeleteItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.DeleteItemOutput, error) { - return &dynamodb.DeleteItemOutput{}, nil - }, - } - service := NewRegistrationService() - - err := service.DeleteRegistration(context.TODO(), mockDynamoDBClient, "eventId", "userId") - - if err != nil { - t.Errorf("expected no error, got %v", err) - } -} diff --git a/functions/gateway/templates/components/navbar.templ b/functions/gateway/templates/components/navbar.templ index 35f0a44e..0370eeec 100644 --- a/functions/gateway/templates/components/navbar.templ +++ b/functions/gateway/templates/components/navbar.templ @@ -516,38 +516,23 @@ templ Navbar(userInfo helpers.UserInfo, subnavTabs []string, event types.Event) console.log(`purchasedItems:`, purchasedItems) // TODO: do we need the total? console.log(`total`, total) - if (purchasedItems?.length < 1) { this.errors['checkout'] = "You have no selected items."; this.reqInFlight = false; return } - let response; - if (total > 0) { - response = await fetch(`/api/checkout/${this.eventId}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - event_name: this.eventName, - purchased_items: purchasedItems, - total: total, - currency: "USD" - }) - }); - } else { - const regResponses = purchasedItems.flatMap(item => item.reg_responses) - response = await fetch(`/api/registrations/${this.eventId}/${this.userId || 'anonymous'}`, { - method: !this.userId ? 'POST' : 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - responses: regResponses, - }) - }); - } + const response = await fetch(`/api/checkout/${this.eventId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + event_name: this.eventName, + purchased_items: purchasedItems, + total: total, + currency: "USD" + }) + }); if (response.status === 401) { this.errors['checkout'] = `You must be logged in to purchase tickets.
Register or Login now`; this.reqInFlight = false; @@ -565,7 +550,9 @@ templ Navbar(userInfo helpers.UserInfo, subnavTabs []string, event types.Event) this.errors['checkout'] = "Invalid checkout URL received."; } } else if (total <= 0) { - window.location.href = window.location.origin + window.location.pathname + "?checkout=registered"; + // `?new_purch_key` is received by the profile page to both highlight, scroll to, and poll in + // wait for the new event because of dynamo's eventual consistency. + window.location.href = window.location.origin + `/admin/profile#registration-history${this.userId ? `?new_purch_key=${json?.composite_key}` : ''}`; } else { if (json?.error?.message) { this.errors['checkout'] = json.error.message; diff --git a/functions/gateway/templates/pages/event_add_edit.templ b/functions/gateway/templates/pages/event_add_edit.templ index fe5d7268..5a6a4607 100644 --- a/functions/gateway/templates/pages/event_add_edit.templ +++ b/functions/gateway/templates/pages/event_add_edit.templ @@ -12,7 +12,7 @@ templ AddOrEditEventPage(pageObj helpers.SitePage, event types.Event, isEditor b

Basic Information

diff --git a/functions/gateway/templates/pages/event_attendees.templ b/functions/gateway/templates/pages/event_attendees.templ index 29fe1d53..148535c6 100644 --- a/functions/gateway/templates/pages/event_attendees.templ +++ b/functions/gateway/templates/pages/event_attendees.templ @@ -28,7 +28,7 @@ templ EventAttendeesPage(pageObj helpers.SitePage, event types.Event, isEditor b

Purchases

@@ -100,8 +100,8 @@ templ EventAttendeesPage(pageObj helpers.SitePage, event types.Event, isEditor b -
-

Registrations

-
-
- - -
-
diff --git a/functions/gateway/templates/pages/event_attendees_templ_test.go b/functions/gateway/templates/pages/event_attendees_templ_test.go index 46756d0e..0e590c19 100644 --- a/functions/gateway/templates/pages/event_attendees_templ_test.go +++ b/functions/gateway/templates/pages/event_attendees_templ_test.go @@ -49,26 +49,6 @@ func TestEventAttendeesPage(t *testing.T) { "data-event-has-registration-fields=\"false\"", }, }, - { - name: "Valid event with registration fields", - pageObj: helpers.SitePage{ - Name: "Event Attendees", - }, - event: types.Event{ - Id: "test-456", - Name: "Test Event", - HasPurchasable: false, - HasRegistrationFields: true, - }, - isEditor: true, - expected: []string{ - "Event Attendees", - "Registrations", - "data-event-id=\"test-456\"", - "data-event-has-purchasable=\"false\"", - "data-event-has-registration-fields=\"true\"", - }, - }, { name: "Valid event with both purchasable and registration fields", pageObj: helpers.SitePage{ @@ -84,7 +64,6 @@ func TestEventAttendeesPage(t *testing.T) { expected: []string{ "Event Attendees", "Purchases", - "Registrations", "data-event-id=\"test-789\"", "data-event-has-purchasable=\"true\"", "data-event-has-registration-fields=\"true\"", diff --git a/functions/gateway/templates/pages/event_details.templ b/functions/gateway/templates/pages/event_details.templ index 2a80da57..2b377ca7 100644 --- a/functions/gateway/templates/pages/event_details.templ +++ b/functions/gateway/templates/pages/event_details.templ @@ -38,7 +38,7 @@ templ IconLeftSection(labelText, labelValue, icon, url string, venueSection bool
} -templ EventDetailsPage(event types.Event, checkoutParamVal string, canEdit bool) { +templ EventDetailsPage(event types.Event, userInfo helpers.UserInfo, checkoutParamVal string, canEdit bool) {
if event.Id == "" {
@@ -58,7 +58,9 @@ templ EventDetailsPage(event types.Event, checkoutParamVal string, canEdit bool)
} else { if (canEdit) { -
You are an editor for this event. Edit Event
+
+ You are an editor for this event. Edit Event +
}

{ event.Name }

if event.EventSourceType == helpers.ES_SERIES_PARENT { @@ -108,9 +110,9 @@ templ EventDetailsPage(event types.Event, checkoutParamVal string, canEdit bool)

- if event.HasPurchasable { + if event.StartingPrice > 0 { - } else if event.HasRegistrationFields { + } else if event.HasRegistrationFields && event.StartingPrice == 0 { } else { RSVP + > + if userInfo.Sub == "" { + ADD TO CALENDAR + } else { + RSVP + } + + } + if userInfo.Sub != "" { +
+
+ + + + } -
-
- - - -
}
event featured image - diff --git a/functions/gateway/templates/pages/profile.templ b/functions/gateway/templates/pages/profile.templ index 492ce9c0..e9390023 100644 --- a/functions/gateway/templates/pages/profile.templ +++ b/functions/gateway/templates/pages/profile.templ @@ -12,7 +12,98 @@ templ ProfilePage(userInfo helpers.UserInfo, roleClaims []helpers.RoleClaim, int
@components.ProfileNav()
-

My Info

+

Event Purchases & Registrations

+
+ + + + + + + + + + + + + + + + + + + + + + +
StatusPurchase Type(s)TotalSignup Time
StatusPurchase Type(s)TotalTime
+
+

My Info

Name: { userInfo.Name }
Email: { userInfo.Email } @@ -71,8 +162,7 @@ templ ProfilePage(userInfo helpers.UserInfo, roleClaims []helpers.RoleClaim, int
-
-

Events Owned by Me

+

Events Owned by Me

// TODO: this is fake data, delete @@ -81,11 +171,10 @@ templ ProfilePage(userInfo helpers.UserInfo, roleClaims []helpers.RoleClaim, int - - Add Your Own Events Soon + Add Your Own Events Soon // //
Date Time Location
//
@@ -155,13 +244,77 @@ templ ProfilePage(userInfo helpers.UserInfo, roleClaims []helpers.RoleClaim, int
- + return { + init() { + (async () => { + window.location.href.match('#') + let searchQuery = window.location.hash.split('?')[1] || ''; + if (!window.location.href.match('#')) { + searchQuery = window.location.search + } + this.newKeyId = new URLSearchParams(searchQuery).get('new_purch_key'); + const maxAttempts = 10; + const delayMs = 250; + let attempts = 0; + + const fetchPurchases = async () => { + const startKey = new URLSearchParams(window.location.search).get('purch_start_key') ?? ''; + const reqUrl = `/api/purchases/user/${this.userId}${ startKey ? `?start_key=${encodeURIComponent(startKey)}` : '' }`; + const purchasesResponse = await fetch(reqUrl); + const purchasesResData = await purchasesResponse.json(); + this.purchases = purchasesResData.purchases ?? []; + this.purchasesNextCursor = purchasesResData?.nextKey?.compositeKey?.Value; + this.hasPurchasesResults = true; + }; + + const poll = async () => { + try { + await fetchPurchases(); + + // If we're not looking for a specific purchase, or if we found it, stop polling + if (!this.newKeyId || this.purchases.some(p => p.composite_key === this.newKeyId)) { + if (this.purchases.some(p => p.composite_key === this.newKeyId)) { + const purchaseRow = document.querySelector(`[data-purch-key="${this.newKeyId}"]`); + if (purchaseRow) { + purchaseRow.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' }); + } + } + return; + } + + // Continue polling if we haven't reached max attempts + if (attempts < maxAttempts) { + attempts++; + await new Promise(resolve => setTimeout(resolve, delayMs)); + await poll(); + } + } catch (error) { + console.error('Failed to fetch purchases:', error); + } + }; + + await poll(); + })() + }, + newKeyId: null, + purchases: [], + hasPurchasesResults: false, + purchasesNextCursor: null, + userId: document.querySelector('#profile-state').getAttribute('data-user-id'), + statusPending: document.querySelector('#profile-state').getAttribute('data-status-pending'), + statusSettled: document.querySelector('#profile-state').getAttribute('data-status-settled'), + getPurchasesNextLink() { + return window.location.href + '?purch_start_key=' + this.purchasesNextCursor + '_' + this.userId; + }, + handleSubdomainPostRes: function(event) { + console.log(event) + }, + getPurchaseStatus(purchase) { + return purchase.status.replace(this.statusSettled, 'PAID').replace(this.statusPending, 'INCOMPLETE') + } + } + } + } diff --git a/functions/gateway/types/event_rsvps.go b/functions/gateway/types/event_rsvps.go deleted file mode 100644 index 758bbb67..00000000 --- a/functions/gateway/types/event_rsvps.go +++ /dev/null @@ -1,52 +0,0 @@ -package types - -import ( - "context" - "time" -) - -// EventRsvpsInsert represents the data required to insert a new user -type EventRsvpInsert struct { - UserID string `json:"user_id" validate:"required" dynamodbav:"userId"` - EventID string `json:"event_id" validate:"required" dynamodbav:"eventId"` - EventSourceType string `json:"event_source_type" validate:"required" dynamodbav:"eventSourceType"` // Validate as email - EventSourceID string `json:"event_source_id" validate:"required" dynamodbav:"eventSourceId"` // Validate as email - Status string `json:"status" validate:"required" dynamodbav:"status"` - CreatedAt time.Time `json:"created_at" dynamodbav:"createdAt"` // Adjust based on your date format - UpdatedAt time.Time `json:"updated_at" dynamodbav:"updatedAt"` // Adjust based on your date format -} - - -// EventRsvps represents a user in the system -type EventRsvp struct { - UserID string `json:"user_id" dynamodbav:"userId"` - EventID string `json:"event_id" dynamodbav:"eventId"` - EventSourceType string `json:"event_source_type" dynamodbav:"eventSourceType"` // Validate as email - EventSourceID string `json:"event_source_id" dynamodbav:"eventSourceId"` // Validate as email - Status string `json:"status" dynamodbav:"status"` - CreatedAt time.Time `json:"created_at" dynamodbav:"createdAt"` // Adjust based on your date format - UpdatedAt time.Time `json:"updated_at" dynamodbav:"updatedAt"` // Adjust based on your date format -} - -// EventRsvpsUpdate represents the data required to update a user -type EventRsvpUpdate struct { - UserID string `json:"user_id" dynamodbav:"userId"` - EventID string `json:"event_id" dynamodbav:"eventId"` - EventSourceID string `json:"event_source_id" dynamodbav:"eventSourceId"` // Validate as email - EventSourceType string `json:"event_source_type" dynamodbav:"eventSourceType"` // Validate as email - Status string `json:"status" dynamodbav:"status"` - UpdatedAt time.Time `json:"updated_at" dynamodbav:"updatedAt"` -} - -// EventRsvpsServiceInterface defines the methods for user-related operations using the RDSDataAPI -type EventRsvpServiceInterface interface { - InsertEventRsvp(ctx context.Context, dynamodbClient DynamoDBAPI, eventRsvp EventRsvpInsert) (*EventRsvp, error) - GetEventRsvpByPk(ctx context.Context, dynamodbClient DynamoDBAPI, eventId, userId string) (*EventRsvp, error) - GetEventRsvpsByUserID(ctx context.Context, dynamodbClient DynamoDBAPI, userId string) ([]EventRsvp, error) - GetEventRsvpsByEventID(ctx context.Context, dynamodbClient DynamoDBAPI, eventId string) ([]EventRsvp, error) - UpdateEventRsvp(ctx context.Context, dynamodbClient DynamoDBAPI, eventId, userId string, eventRsvp EventRsvpUpdate) (*EventRsvp, error) - DeleteEventRsvp(ctx context.Context, dynamodbClient DynamoDBAPI, eventId, userId string) error -} - - - diff --git a/functions/gateway/types/purchases.go b/functions/gateway/types/purchases.go index 819fb5d3..f5321709 100644 --- a/functions/gateway/types/purchases.go +++ b/functions/gateway/types/purchases.go @@ -30,8 +30,8 @@ type PurchaseInsert struct { EventName string `json:"event_name" validate:"required" dynamodbav:"eventName"` Status string `json:"status" validate:"required" dynamodbav:"status"` PurchasedItems []PurchasedItem `json:"purchased_items" validate:"required" dynamodbav:"purchasedItems"` - Total int32 `json:"total" validate:"required" dynamodbav:"total"` - Currency string `json:"currency" validate:"required" dynamodbav:"currency"` + Total int32 `json:"total" dynamodbav:"total"` + Currency string `json:"currency" dynamodbav:"currency"` StripeSessionId string `json:"stripe_session_id" dynamodbav:"stripeSessionId"` StripeTransactionId string `json:"stripe_transaction_id" dynamodbav:"stripeTransactionId"` CreatedAt int64 `json:"created_at" validate:"required" dynamodbav:"createdAt"` // Adjust based on your date format diff --git a/functions/gateway/types/registrations.go b/functions/gateway/types/registrations.go deleted file mode 100644 index a7d446b8..00000000 --- a/functions/gateway/types/registrations.go +++ /dev/null @@ -1,44 +0,0 @@ -package types - -import ( - "context" - "time" - - dynamodb_types "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" -) - -// RegistrationsInsert represents the data required to insert a new user -type RegistrationInsert struct { - EventId string `json:"event_id" validate:"required" dynamodbav:"eventId"` // UUID format validation - UserId string `json:"user_id" validate:"required" dynamodbav:"userId"` - Responses []map[string]interface{} `json:"responses" dynamodbav:"responses"` - CreatedAt time.Time `json:"created_at" dynamodbav:"createdAt"` // Adjust based on your date format - UpdatedAt time.Time `json:"updated_at" dynamodbav:"updatedAt"` // Adjust based on your date format -} - -// Registrations represents a user in the system -type Registration struct { - EventId string `json:"event_id" validate:"required" dynamodbav:"eventId` // UUID format validation - UserId string `json:"user_id" validate:"required" dynamodbav:"userId"` - Responses []map[string]interface{} `json:"responses" dynamodbav:"responses"` - CreatedAt time.Time `json:"created_at" dynamodbav:"createdAt"` // Adjust based on your date format - UpdatedAt time.Time `json:"updated_at" dynamodbav:"updatedAt"` // Adjust based on your date format -} - -// RegistrationsUpdate represents the data required to update a user -type RegistrationUpdate struct { - EventId string `json:"event_id" validate:"required" dynamodbav:"eventId` // UUID format validation - UserId string `json:"user_id" validate:"required" dynamodbav:"userId"` - Responses []map[string]interface{} `json:"responses" dynamodbav:"responses"` - UpdatedAt time.Time `json:"updated_at" dynamodbav:"updatedAt"` // Adjust based on your date format -} - -// RegistrationsServiceInterface defines the methods for user-related operations using the RDSDataAPI -type RegistrationServiceInterface interface { - InsertRegistration(ctx context.Context, dynamoClient DynamoDBAPI, registration RegistrationInsert, eventId, userId string) (*Registration, error) - GetRegistrationByPk(ctx context.Context, dynamoClient DynamoDBAPI, eventId, userId string) (*Registration, error) - GetRegistrationsByUserID(ctx context.Context, dynamoClient DynamoDBAPI, userId string) ([]Registration, error) - GetRegistrationsByEventID(ctx context.Context, dynamoClient DynamoDBAPI, eventId string, limit int32, startKey string) ([]Registration, map[string]dynamodb_types.AttributeValue, error) - UpdateRegistration(ctx context.Context, dynamoClient DynamoDBAPI, eventId, userId string, registration RegistrationUpdate) (*Registration, error) - DeleteRegistration(ctx context.Context, dynamoClient DynamoDBAPI, eventId, userId string) error -} diff --git a/package.json b/package.json index 4b877fc0..76db92bd 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "deploy:prod": "sst deploy --stage prod", "remove": "sst remove", "console": "sst console", - "tailwind:prod": "tailwindcss -i ./static/assets/global.css -o ./static/assets/styles.css --minify", + "tailwind:prod": "NODE_ENV=production tailwindcss --postcss -i ./static/assets/global.css -o ./static/assets/styles.css --minify", "typecheck": "tsc --noEmit" }, "devDependencies": { diff --git a/postcss.config.js b/postcss.config.js index 6cfe9c10..7f838842 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -6,71 +6,99 @@ import path from 'path'; let previousHash = ''; let previousTemplateHash = ''; +function updateCSSFiles(css, result) { + const resultString = JSON.stringify(result.messages); + const combinedContent = css + resultString; + const newHash = crypto.createHash('md5').update(combinedContent).digest('hex').slice(0, 8); + + // Define paths + const baseStylesPath = './static/assets/styles'; + const tempFile = `${baseStylesPath}.css`; + + // Add initial file creation + if (!fs.existsSync(tempFile)) { + console.log('Creating initial styles.css file'); + fs.writeFileSync(tempFile, css); + } + + const newFileName = `${baseStylesPath}.${newHash}.css`; + const templatePath = 'functions/gateway/templates/pages/layout.templ'; + // Always copy the file in production mode + if (process.env.NODE_ENV === 'production') { + if (fs.existsSync(tempFile)) { + fs.copyFileSync(tempFile, newFileName); + + // Update template with new hash + if (fs.existsSync(templatePath)) { + const template = fs.readFileSync(templatePath, 'utf8'); + const pattern = /styles\..*?\.css/; + const updatedTemplate = template.replace(pattern, `styles.${newHash}.css`); + fs.writeFileSync(templatePath, updatedTemplate); + } + + console.log(`📦 Production CSS generated: ${newFileName}`); + return; + } + } + + // Rest of the watch mode logic + try { + // Read the template file + const template = fs.readFileSync(templatePath, 'utf8'); + + // Hash the template content to detect changes + const templateHash = crypto.createHash('md5').update(template).digest('hex').slice(0, 8); + + // Extract current hash from filename in template + const hashMatch = template.match(/styles\.(.*?)\.css/); + const currentHash = hashMatch ? hashMatch[1] : null; + + // Check if we need to update based on: + // 1. Hash differences + // 2. Missing hashed CSS file + // 3. Template content hasn't changed + const currentHashedFile = currentHash ? `${baseStylesPath}.${currentHash}.css` : null; + const needsUpdate = (newHash !== currentHash && newHash !== previousHash || + (currentHash && !fs.existsSync(currentHashedFile))) && + templateHash !== previousTemplateHash; + + if (needsUpdate) { + previousTemplateHash = templateHash; // Store the new template hash + const pattern = /styles\..*?\.css/; + const updatedTemplate = template.replace(pattern, `styles.${newHash}.css`); + fs.writeFileSync(templatePath, updatedTemplate); + + // Ensure the temp file exists and copy it + if (fs.existsSync(tempFile)) { + fs.copyFileSync(tempFile, newFileName); + + // Remove old CSS file if it exists + if (previousHash) { + const oldFile = `${baseStylesPath}.${previousHash}.css`; + if (fs.existsSync(oldFile)) { + fs.unlinkSync(oldFile); + } + } + + console.log(`🔄 CSS updated: ${newFileName}`); + } else { + console.warn('Warning: styles.css not found for initial copy'); + } + } + + previousHash = newHash; + } catch (error) { + console.warn('Warning: Could not update CSS:', error.message); + } +} + export default { plugins: [ tailwindcss, { postcssPlugin: 'css-watch-logger', Once(root, { result }) { - try { - const css = root.toString(); - const resultString = JSON.stringify(result.messages); - const combinedContent = css + resultString; - const newHash = crypto.createHash('md5').update(combinedContent).digest('hex').slice(0, 8); - - // Define paths - const baseStylesPath = './static/assets/styles'; - const tempFile = `${baseStylesPath}.css`; - const newFileName = `${baseStylesPath}.${newHash}.css`; - - // Read the template file - const templatePath = 'functions/gateway/templates/pages/layout.templ'; - const template = fs.readFileSync(templatePath, 'utf8'); - - // Hash the template content to detect changes - const templateHash = crypto.createHash('md5').update(template).digest('hex').slice(0, 8); - - // Extract current hash from filename in template - const hashMatch = template.match(/styles\.(.*?)\.css/); - const currentHash = hashMatch ? hashMatch[1] : null; - - // Check if we need to update based on: - // 1. Hash differences - // 2. Missing hashed CSS file - // 3. Template content hasn't changed - const currentHashedFile = currentHash ? `${baseStylesPath}.${currentHash}.css` : null; - const needsUpdate = (newHash !== currentHash && newHash !== previousHash || - (currentHash && !fs.existsSync(currentHashedFile))) && - templateHash !== previousTemplateHash; - - if (needsUpdate) { - previousTemplateHash = templateHash; // Store the new template hash - const pattern = /styles\..*?\.css/; - const updatedTemplate = template.replace(pattern, `styles.${newHash}.css`); - fs.writeFileSync(templatePath, updatedTemplate); - - // Ensure the temp file exists and copy it - if (fs.existsSync(tempFile)) { - fs.copyFileSync(tempFile, newFileName); - - // Remove old CSS file if it exists - if (previousHash) { - const oldFile = `${baseStylesPath}.${previousHash}.css`; - if (fs.existsSync(oldFile)) { - fs.unlinkSync(oldFile); - } - } - - console.log(`🔄 CSS updated: ${newFileName}`); - } else { - console.warn('Warning: styles.css not found for initial copy'); - } - } - - previousHash = newHash; - } catch (error) { - console.warn('Warning: Could not update CSS:', error.message); - } + updateCSSFiles(root.toString(), result); } } ] diff --git a/stacks/StaticSiteStack.ts b/stacks/StaticSiteStack.ts index cfe26a79..eb0ee137 100644 --- a/stacks/StaticSiteStack.ts +++ b/stacks/StaticSiteStack.ts @@ -11,7 +11,7 @@ export function StaticSiteStack({ stack }: StackContext) { dev: { deploy: true, }, - buildCommand: 'npm run tailwind:prod', + buildCommand: 'NODE_ENV=production npm run tailwind:prod', }); stack.addOutputs({ StaticEndpoint: staticSite?.url, diff --git a/stacks/StorageStack.ts b/stacks/StorageStack.ts index e7dfdbaf..3f63a607 100644 --- a/stacks/StorageStack.ts +++ b/stacks/StorageStack.ts @@ -1,8 +1,7 @@ import { StackContext, Table } from 'sst/constructs'; export function StorageStack({ stack }: StackContext) { - // Create the `Registrations` table - // + // 🚨 WARNING 🚨 Deprecated, do not use const eventRsvpsTable = new Table(stack, 'EventRsvps', { fields: { id: 'string', @@ -72,6 +71,7 @@ export function StorageStack({ stack }: StackContext) { primaryIndex: { partitionKey: 'eventId' }, }); + // 🚨 WARNING 🚨 Deprecated, do not use const registrationsTable = new Table(stack, 'Registrations', { fields: { eventId: 'string', diff --git a/static/assets/global.css b/static/assets/global.css index f8bafc32..9439303e 100644 --- a/static/assets/global.css +++ b/static/assets/global.css @@ -146,8 +146,9 @@ body:has(.bottom-drawer) { display: none; } -.margins-when-children.my-8:has(*) { - margin: 2rem 0; +.margins-when-children:not(:has(:first-child)) { + margin-top: 0 !important; + margin-bottom: 0 !important; } /* BEGIN seshu ingestion "add event source" section */ diff --git a/static/assets/styles.59a94be0.css b/static/assets/styles.59a94be0.css deleted file mode 100644 index 272d1984..00000000 --- a/static/assets/styles.59a94be0.css +++ /dev/null @@ -1,5871 +0,0 @@ -/* -! tailwindcss v3.4.9 | MIT License | https://tailwindcss.com -*/ - -/* -1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) -2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) -*/ - -*, -::before, -::after { - box-sizing: border-box; - /* 1 */ - border-width: 0; - /* 2 */ - border-style: solid; - /* 2 */ - border-color: #e5e7eb; - /* 2 */ -} - -::before, -::after { - --tw-content: ''; -} - -/* -1. Use a consistent sensible line-height in all browsers. -2. Prevent adjustments of font size after orientation changes in iOS. -3. Use a more readable tab size. -4. Use the user's configured `sans` font-family by default. -5. Use the user's configured `sans` font-feature-settings by default. -6. Use the user's configured `sans` font-variation-settings by default. -7. Disable tap highlights on iOS -*/ - -html, -:host { - line-height: 1.5; - /* 1 */ - -webkit-text-size-adjust: 100%; - /* 2 */ - -moz-tab-size: 4; - /* 3 */ - -o-tab-size: 4; - tab-size: 4; - /* 3 */ - font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; - /* 4 */ - font-feature-settings: normal; - /* 5 */ - font-variation-settings: normal; - /* 6 */ - -webkit-tap-highlight-color: transparent; - /* 7 */ -} - -/* -1. Remove the margin in all browsers. -2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. -*/ - -body { - margin: 0; - /* 1 */ - line-height: inherit; - /* 2 */ -} - -/* -1. Add the correct height in Firefox. -2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) -3. Ensure horizontal rules are visible by default. -*/ - -hr { - height: 0; - /* 1 */ - color: inherit; - /* 2 */ - border-top-width: 1px; - /* 3 */ -} - -/* -Add the correct text decoration in Chrome, Edge, and Safari. -*/ - -abbr:where([title]) { - -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; -} - -/* -Remove the default font size and weight for headings. -*/ - -h1, -h2, -h3, -h4, -h5, -h6 { - font-size: inherit; - font-weight: inherit; -} - -/* -Reset links to optimize for opt-in styling instead of opt-out. -*/ - -a { - color: inherit; - text-decoration: inherit; -} - -/* -Add the correct font weight in Edge and Safari. -*/ - -b, -strong { - font-weight: bolder; -} - -/* -1. Use the user's configured `mono` font-family by default. -2. Use the user's configured `mono` font-feature-settings by default. -3. Use the user's configured `mono` font-variation-settings by default. -4. Correct the odd `em` font sizing in all browsers. -*/ - -code, -kbd, -samp, -pre { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - /* 1 */ - font-feature-settings: normal; - /* 2 */ - font-variation-settings: normal; - /* 3 */ - font-size: 1em; - /* 4 */ -} - -/* -Add the correct font size in all browsers. -*/ - -small { - font-size: 80%; -} - -/* -Prevent `sub` and `sup` elements from affecting the line height in all browsers. -*/ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -/* -1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) -2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) -3. Remove gaps between table borders by default. -*/ - -table { - text-indent: 0; - /* 1 */ - border-color: inherit; - /* 2 */ - border-collapse: collapse; - /* 3 */ -} - -/* -1. Change the font styles in all browsers. -2. Remove the margin in Firefox and Safari. -3. Remove default padding in all browsers. -*/ - -button, -input, -optgroup, -select, -textarea { - font-family: inherit; - /* 1 */ - font-feature-settings: inherit; - /* 1 */ - font-variation-settings: inherit; - /* 1 */ - font-size: 100%; - /* 1 */ - font-weight: inherit; - /* 1 */ - line-height: inherit; - /* 1 */ - letter-spacing: inherit; - /* 1 */ - color: inherit; - /* 1 */ - margin: 0; - /* 2 */ - padding: 0; - /* 3 */ -} - -/* -Remove the inheritance of text transform in Edge and Firefox. -*/ - -button, -select { - text-transform: none; -} - -/* -1. Correct the inability to style clickable types in iOS and Safari. -2. Remove default button styles. -*/ - -button, -input:where([type='button']), -input:where([type='reset']), -input:where([type='submit']) { - -webkit-appearance: button; - /* 1 */ - background-color: transparent; - /* 2 */ - background-image: none; - /* 2 */ -} - -/* -Use the modern Firefox focus style for all focusable elements. -*/ - -:-moz-focusring { - outline: auto; -} - -/* -Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) -*/ - -:-moz-ui-invalid { - box-shadow: none; -} - -/* -Add the correct vertical alignment in Chrome and Firefox. -*/ - -progress { - vertical-align: baseline; -} - -/* -Correct the cursor style of increment and decrement buttons in Safari. -*/ - -::-webkit-inner-spin-button, -::-webkit-outer-spin-button { - height: auto; -} - -/* -1. Correct the odd appearance in Chrome and Safari. -2. Correct the outline style in Safari. -*/ - -[type='search'] { - -webkit-appearance: textfield; - /* 1 */ - outline-offset: -2px; - /* 2 */ -} - -/* -Remove the inner padding in Chrome and Safari on macOS. -*/ - -::-webkit-search-decoration { - -webkit-appearance: none; -} - -/* -1. Correct the inability to style clickable types in iOS and Safari. -2. Change font properties to `inherit` in Safari. -*/ - -::-webkit-file-upload-button { - -webkit-appearance: button; - /* 1 */ - font: inherit; - /* 2 */ -} - -/* -Add the correct display in Chrome and Safari. -*/ - -summary { - display: list-item; -} - -/* -Removes the default spacing and border for appropriate elements. -*/ - -blockquote, -dl, -dd, -h1, -h2, -h3, -h4, -h5, -h6, -hr, -figure, -p, -pre { - margin: 0; -} - -fieldset { - margin: 0; - padding: 0; -} - -legend { - padding: 0; -} - -ol, -ul, -menu { - list-style: none; - margin: 0; - padding: 0; -} - -/* -Reset default styling for dialogs. -*/ - -dialog { - padding: 0; -} - -/* -Prevent resizing textareas horizontally by default. -*/ - -textarea { - resize: vertical; -} - -/* -1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) -2. Set the default placeholder color to the user's configured gray 400 color. -*/ - -input::-moz-placeholder, textarea::-moz-placeholder { - opacity: 1; - /* 1 */ - color: #9ca3af; - /* 2 */ -} - -input::placeholder, -textarea::placeholder { - opacity: 1; - /* 1 */ - color: #9ca3af; - /* 2 */ -} - -/* -Set the default cursor for buttons. -*/ - -button, -[role="button"] { - cursor: pointer; -} - -/* -Make sure disabled buttons don't get the pointer cursor. -*/ - -:disabled { - cursor: default; -} - -/* -1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) -2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) - This can trigger a poorly considered lint error in some tools but is included by design. -*/ - -img, -svg, -video, -canvas, -audio, -iframe, -embed, -object { - display: block; - /* 1 */ - vertical-align: middle; - /* 2 */ -} - -/* -Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) -*/ - -img, -video { - max-width: 100%; - height: auto; -} - -/* Make elements with the HTML hidden attribute stay hidden by default */ - -[hidden] { - display: none; -} - -:root, -[data-theme] { - background-color: var(--fallback-b1,oklch(var(--b1)/1)); - color: var(--fallback-bc,oklch(var(--bc)/1)); -} - -@supports not (color: oklch(0% 0 0)) { - :root { - color-scheme: light; - --fallback-p: #491eff; - --fallback-pc: #d4dbff; - --fallback-s: #ff41c7; - --fallback-sc: #fff9fc; - --fallback-a: #00cfbd; - --fallback-ac: #00100d; - --fallback-n: #2b3440; - --fallback-nc: #d7dde4; - --fallback-b1: #ffffff; - --fallback-b2: #e5e6e6; - --fallback-b3: #e5e6e6; - --fallback-bc: #1f2937; - --fallback-in: #00b3f0; - --fallback-inc: #000000; - --fallback-su: #00ca92; - --fallback-suc: #000000; - --fallback-wa: #ffc22d; - --fallback-wac: #000000; - --fallback-er: #ff6f70; - --fallback-erc: #000000; - } - - @media (prefers-color-scheme: dark) { - :root { - color-scheme: dark; - --fallback-p: #7582ff; - --fallback-pc: #050617; - --fallback-s: #ff71cf; - --fallback-sc: #190211; - --fallback-a: #00c7b5; - --fallback-ac: #000e0c; - --fallback-n: #2a323c; - --fallback-nc: #a6adbb; - --fallback-b1: #1d232a; - --fallback-b2: #191e24; - --fallback-b3: #15191e; - --fallback-bc: #a6adbb; - --fallback-in: #00b3f0; - --fallback-inc: #000000; - --fallback-su: #00ca92; - --fallback-suc: #000000; - --fallback-wa: #ffc22d; - --fallback-wac: #000000; - --fallback-er: #ff6f70; - --fallback-erc: #000000; - } - } -} - -html { - -webkit-tap-highlight-color: transparent; -} - -* { - scrollbar-color: color-mix(in oklch, currentColor 35%, transparent) transparent; -} - -*:hover { - scrollbar-color: color-mix(in oklch, currentColor 60%, transparent) transparent; -} - -:root { - color-scheme: dark; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --p: 87.1432% 0.285969 141.530703; - --s: 66.0199% 0.229356 35.402514; - --a: 72.8297% 0.197075 351.994708; - --n: 84.5222% 0 0; - --nc: 0% 0 0; - --b1: 0% 0 0; - --b2: 28.5017% 0 0; - --b3: 39.0421% 0 0; - --bc: 97.0151% 0 0; - font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace; - --rounded-box: 0.5rem; - --rounded-btn: 0.25rem; - --rounded-badge: 1rem; - --tab-radius: 0.25rem; - --pc: 17.4328% 0.057824 141.37345; - --sc: 94.9119% 0 0; - --ac: 14.6655% 0.038503 352.331265; - --in: 76.888% 0.097944 243.969204; - --inc: 0% 0 0; - --su: 83.9524% 0.205841 141.105446; - --suc: 0% 0 0; - --wa: 85.3968% 0.140313 79.943272; - --wac: 0% 0 0; - --er: 58.5838% 0.222042 17.584628; - --erc: 0% 0 0; -} - -h1, - h2, - h3, - h4, - h5, - h6 { - font-family: 'Outfit', 'Helvetica', 'Arial', 'sans-serif'; - font-weight: 400; -} - -h1.title, - h2.title, - h3.title, - h4.title, - h5.title, - h6.title { - letter-spacing: 0.15rem; - text-transform: uppercase; -} - -*, ::before, ::after { - --tw-border-spacing-x: 0; - --tw-border-spacing-y: 0; - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-rotate: 0; - --tw-skew-x: 0; - --tw-skew-y: 0; - --tw-scale-x: 1; - --tw-scale-y: 1; - --tw-pan-x: ; - --tw-pan-y: ; - --tw-pinch-zoom: ; - --tw-scroll-snap-strictness: proximity; - --tw-gradient-from-position: ; - --tw-gradient-via-position: ; - --tw-gradient-to-position: ; - --tw-ordinal: ; - --tw-slashed-zero: ; - --tw-numeric-figure: ; - --tw-numeric-spacing: ; - --tw-numeric-fraction: ; - --tw-ring-inset: ; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-color: rgb(59 130 246 / 0.5); - --tw-ring-offset-shadow: 0 0 #0000; - --tw-ring-shadow: 0 0 #0000; - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - --tw-blur: ; - --tw-brightness: ; - --tw-contrast: ; - --tw-grayscale: ; - --tw-hue-rotate: ; - --tw-invert: ; - --tw-saturate: ; - --tw-sepia: ; - --tw-drop-shadow: ; - --tw-backdrop-blur: ; - --tw-backdrop-brightness: ; - --tw-backdrop-contrast: ; - --tw-backdrop-grayscale: ; - --tw-backdrop-hue-rotate: ; - --tw-backdrop-invert: ; - --tw-backdrop-opacity: ; - --tw-backdrop-saturate: ; - --tw-backdrop-sepia: ; - --tw-contain-size: ; - --tw-contain-layout: ; - --tw-contain-paint: ; - --tw-contain-style: ; -} - -::backdrop { - --tw-border-spacing-x: 0; - --tw-border-spacing-y: 0; - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-rotate: 0; - --tw-skew-x: 0; - --tw-skew-y: 0; - --tw-scale-x: 1; - --tw-scale-y: 1; - --tw-pan-x: ; - --tw-pan-y: ; - --tw-pinch-zoom: ; - --tw-scroll-snap-strictness: proximity; - --tw-gradient-from-position: ; - --tw-gradient-via-position: ; - --tw-gradient-to-position: ; - --tw-ordinal: ; - --tw-slashed-zero: ; - --tw-numeric-figure: ; - --tw-numeric-spacing: ; - --tw-numeric-fraction: ; - --tw-ring-inset: ; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-color: rgb(59 130 246 / 0.5); - --tw-ring-offset-shadow: 0 0 #0000; - --tw-ring-shadow: 0 0 #0000; - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - --tw-blur: ; - --tw-brightness: ; - --tw-contrast: ; - --tw-grayscale: ; - --tw-hue-rotate: ; - --tw-invert: ; - --tw-saturate: ; - --tw-sepia: ; - --tw-drop-shadow: ; - --tw-backdrop-blur: ; - --tw-backdrop-brightness: ; - --tw-backdrop-contrast: ; - --tw-backdrop-grayscale: ; - --tw-backdrop-hue-rotate: ; - --tw-backdrop-invert: ; - --tw-backdrop-opacity: ; - --tw-backdrop-saturate: ; - --tw-backdrop-sepia: ; - --tw-contain-size: ; - --tw-contain-layout: ; - --tw-contain-paint: ; - --tw-contain-style: ; -} - -.alert { - display: grid; - width: 100%; - grid-auto-flow: row; - align-content: flex-start; - align-items: center; - justify-items: center; - gap: 1rem; - text-align: center; - border-radius: var(--rounded-box, 1rem); - border-width: 1px; - --tw-border-opacity: 1; - border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); - padding: 1rem; - --tw-text-opacity: 1; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); - --alert-bg: var(--fallback-b2,oklch(var(--b2)/1)); - --alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1)); - background-color: var(--alert-bg); -} - -@media (min-width: 640px) { - .alert { - grid-auto-flow: column; - grid-template-columns: auto minmax(auto,1fr); - justify-items: start; - text-align: start; - } -} - -.avatar { - position: relative; - display: inline-flex; -} - -.avatar > div { - display: block; - aspect-ratio: 1 / 1; - overflow: hidden; -} - -.avatar img { - height: 100%; - width: 100%; - -o-object-fit: cover; - object-fit: cover; -} - -.avatar.placeholder > div { - display: flex; - align-items: center; - justify-content: center; -} - -.badge { - display: inline-flex; - align-items: center; - justify-content: center; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); - transition-duration: 200ms; - height: 1.25rem; - font-size: 0.875rem; - line-height: 1.25rem; - width: -moz-fit-content; - width: fit-content; - padding-left: 0.563rem; - padding-right: 0.563rem; - border-radius: var(--rounded-badge, 1.9rem); - border-width: 1px; - --tw-border-opacity: 1; - border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); - --tw-bg-opacity: 1; - background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); -} - -@media (hover:hover) { - .label a:hover { - --tw-text-opacity: 1; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); - } - - .menu li > *:not(ul, .menu-title, details, .btn):active, -.menu li > *:not(ul, .menu-title, details, .btn).active, -.menu li > details > summary:active { - --tw-bg-opacity: 1; - background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); - } - - .tab:hover { - --tw-text-opacity: 1; - } - - .table tr.hover:hover, - .table tr.hover:nth-child(even):hover { - --tw-bg-opacity: 1; - background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); - } - - .table-zebra tr.hover:hover, - .table-zebra tr.hover:nth-child(even):hover { - --tw-bg-opacity: 1; - background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity))); - } -} - -.btn { - display: inline-flex; - height: 3rem; - min-height: 3rem; - flex-shrink: 0; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - flex-wrap: wrap; - align-items: center; - justify-content: center; - border-radius: var(--rounded-btn, 0.5rem); - border-color: transparent; - border-color: oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity)); - padding-left: 1rem; - padding-right: 1rem; - text-align: center; - font-size: 0.875rem; - line-height: 1em; - gap: 0.5rem; - font-weight: 600; - text-decoration-line: none; - transition-duration: 200ms; - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); - border-width: var(--border-btn, 1px); - transition-property: color, background-color, border-color, opacity, box-shadow, transform; - --tw-text-opacity: 1; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); - --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); - --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); - outline-color: var(--fallback-bc,oklch(var(--bc)/1)); - background-color: oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity)); - --tw-bg-opacity: 1; - --tw-border-opacity: 1; -} - -.btn-disabled, - .btn[disabled], - .btn:disabled { - pointer-events: none; -} - -.btn-square { - height: 3rem; - width: 3rem; - padding: 0px; -} - -.btn-circle { - height: 3rem; - width: 3rem; - border-radius: 9999px; - padding: 0px; -} - -:where(.btn:is(input[type="checkbox"])), -:where(.btn:is(input[type="radio"])) { - width: auto; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} - -.btn:is(input[type="checkbox"]):after, -.btn:is(input[type="radio"]):after { - --tw-content: attr(aria-label); - content: var(--tw-content); -} - -.card { - position: relative; - display: flex; - flex-direction: column; - border-radius: var(--rounded-box, 1rem); -} - -.card:focus { - outline: 2px solid transparent; - outline-offset: 2px; -} - -.card-body { - display: flex; - flex: 1 1 auto; - flex-direction: column; - padding: var(--padding-card, 2rem); - gap: 0.5rem; -} - -.card-body :where(p) { - flex-grow: 1; -} - -.card figure { - display: flex; - align-items: center; - justify-content: center; -} - -.card.image-full { - display: grid; -} - -.card.image-full:before { - position: relative; - content: ""; - z-index: 10; - border-radius: var(--rounded-box, 1rem); - --tw-bg-opacity: 1; - background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); - opacity: 0.75; -} - -.card.image-full:before, - .card.image-full > * { - grid-column-start: 1; - grid-row-start: 1; -} - -.card.image-full > figure img { - height: 100%; - -o-object-fit: cover; - object-fit: cover; -} - -.card.image-full > .card-body { - position: relative; - z-index: 20; - --tw-text-opacity: 1; - color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); -} - -.carousel { - display: inline-flex; - overflow-x: scroll; - scroll-snap-type: x mandatory; - scroll-behavior: smooth; - -ms-overflow-style: none; - scrollbar-width: none; -} - -.carousel-item { - box-sizing: content-box; - display: flex; - flex: none; - scroll-snap-align: start; -} - -.carousel-start .carousel-item { - scroll-snap-align: start; -} - -.carousel-center .carousel-item { - scroll-snap-align: center; -} - -.carousel-end .carousel-item { - scroll-snap-align: end; -} - -.checkbox { - flex-shrink: 0; - --chkbg: var(--fallback-bc,oklch(var(--bc)/1)); - --chkfg: var(--fallback-b1,oklch(var(--b1)/1)); - height: 1.5rem; - width: 1.5rem; - cursor: pointer; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - border-radius: var(--rounded-btn, 0.5rem); - border-width: 1px; - border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity))); - --tw-border-opacity: 0.2; -} - -.collapse:not(td):not(tr):not(colgroup) { - visibility: visible; -} - -.collapse { - position: relative; - display: grid; - overflow: hidden; - grid-template-rows: auto 0fr; - transition: grid-template-rows 0.2s; - width: 100%; - border-radius: var(--rounded-box, 1rem); -} - -.collapse-title, -.collapse > input[type="checkbox"], -.collapse > input[type="radio"], -.collapse-content { - grid-column-start: 1; - grid-row-start: 1; -} - -.collapse > input[type="checkbox"], -.collapse > input[type="radio"] { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - opacity: 0; -} - -.collapse-content { - visibility: hidden; - grid-column-start: 1; - grid-row-start: 2; - min-height: 0px; - transition: visibility 0.2s; - transition: padding 0.2s ease-out, - background-color 0.2s ease-out; - padding-left: 1rem; - padding-right: 1rem; - cursor: unset; -} - -.collapse[open], -.collapse-open, -.collapse:focus:not(.collapse-close) { - grid-template-rows: auto 1fr; -} - -.collapse:not(.collapse-close):has(> input[type="checkbox"]:checked), -.collapse:not(.collapse-close):has(> input[type="radio"]:checked) { - grid-template-rows: auto 1fr; -} - -.collapse[open] > .collapse-content, -.collapse-open > .collapse-content, -.collapse:focus:not(.collapse-close) > .collapse-content, -.collapse:not(.collapse-close) > input[type="checkbox"]:checked ~ .collapse-content, -.collapse:not(.collapse-close) > input[type="radio"]:checked ~ .collapse-content { - visibility: visible; - min-height: -moz-fit-content; - min-height: fit-content; -} - -.divider { - display: flex; - flex-direction: row; - align-items: center; - align-self: stretch; - margin-top: 1rem; - margin-bottom: 1rem; - height: 1rem; - white-space: nowrap; -} - -.divider:before, - .divider:after { - height: 0.125rem; - width: 100%; - flex-grow: 1; - --tw-content: ''; - content: var(--tw-content); - background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); -} - -.drawer { - position: relative; - display: grid; - grid-auto-columns: max-content auto; - width: 100%; -} - -.drawer-content { - grid-column-start: 2; - grid-row-start: 1; - min-width: 0px; -} - -.drawer-side { - pointer-events: none; - position: fixed; - inset-inline-start: 0px; - top: 0px; - grid-column-start: 1; - grid-row-start: 1; - display: grid; - width: 100%; - grid-template-columns: repeat(1, minmax(0, 1fr)); - grid-template-rows: repeat(1, minmax(0, 1fr)); - align-items: flex-start; - justify-items: start; - overflow-x: hidden; - overflow-y: hidden; - overscroll-behavior: contain; - height: 100vh; - height: 100dvh; -} - -.drawer-side > .drawer-overlay { - position: sticky; - top: 0px; - place-self: stretch; - cursor: pointer; - background-color: transparent; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); - transition-duration: 200ms; -} - -.drawer-side > * { - grid-column-start: 1; - grid-row-start: 1; -} - -.drawer-side > *:not(.drawer-overlay) { - transition-property: transform; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); - transition-duration: 300ms; - will-change: transform; - transform: translateX(-100%); -} - -[dir="rtl"] .drawer-side > *:not(.drawer-overlay) { - transform: translateX(100%); -} - -.drawer-toggle { - position: fixed; - height: 0px; - width: 0px; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - opacity: 0; -} - -.drawer-toggle:checked ~ .drawer-side { - pointer-events: auto; - visibility: visible; - overflow-y: auto; -} - -.drawer-toggle:checked ~ .drawer-side > *:not(.drawer-overlay) { - transform: translateX(0%); -} - -.drawer-end { - grid-auto-columns: auto max-content; -} - -.drawer-end .drawer-toggle ~ .drawer-content { - grid-column-start: 1; -} - -.drawer-end .drawer-toggle ~ .drawer-side { - grid-column-start: 2; - justify-items: end; -} - -.drawer-end .drawer-toggle ~ .drawer-side > *:not(.drawer-overlay) { - transform: translateX(100%); -} - -[dir="rtl"] .drawer-end .drawer-toggle ~ .drawer-side > *:not(.drawer-overlay) { - transform: translateX(-100%); -} - -.drawer-end .drawer-toggle:checked ~ .drawer-side > *:not(.drawer-overlay) { - transform: translateX(0%); -} - -.dropdown { - position: relative; - display: inline-block; -} - -.dropdown > *:not(summary):focus { - outline: 2px solid transparent; - outline-offset: 2px; -} - -.dropdown .dropdown-content { - position: absolute; -} - -.dropdown:is(:not(details)) .dropdown-content { - visibility: hidden; - opacity: 0; - transform-origin: top; - --tw-scale-x: .95; - --tw-scale-y: .95; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); - transition-duration: 200ms; -} - -.dropdown-end .dropdown-content { - inset-inline-end: 0px; -} - -.dropdown-left .dropdown-content { - bottom: auto; - inset-inline-end: 100%; - top: 0px; - transform-origin: right; -} - -.dropdown-right .dropdown-content { - bottom: auto; - inset-inline-start: 100%; - top: 0px; - transform-origin: left; -} - -.dropdown-bottom .dropdown-content { - bottom: auto; - top: 100%; - transform-origin: top; -} - -.dropdown-top .dropdown-content { - bottom: 100%; - top: auto; - transform-origin: bottom; -} - -.dropdown-end.dropdown-right .dropdown-content { - bottom: 0px; - top: auto; -} - -.dropdown-end.dropdown-left .dropdown-content { - bottom: 0px; - top: auto; -} - -.dropdown.dropdown-open .dropdown-content, -.dropdown:not(.dropdown-hover):focus .dropdown-content, -.dropdown:focus-within .dropdown-content { - visibility: visible; - opacity: 1; -} - -@media (hover: hover) { - .dropdown.dropdown-hover:hover .dropdown-content { - visibility: visible; - opacity: 1; - } - - .btm-nav > *.disabled:hover, - .btm-nav > *[disabled]:hover { - pointer-events: none; - --tw-border-opacity: 0; - background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); - --tw-bg-opacity: 0.1; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); - --tw-text-opacity: 0.2; - } - - .btn:hover { - --tw-border-opacity: 1; - border-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity))); - --tw-bg-opacity: 1; - background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity))); - } - - @supports (color: color-mix(in oklab, black, black)) { - .btn:hover { - background-color: color-mix( - in oklab, - oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity, 1)) 90%, - black - ); - border-color: color-mix( - in oklab, - oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity, 1)) 90%, - black - ); - } - } - - @supports not (color: oklch(0% 0 0)) { - .btn:hover { - background-color: var(--btn-color, var(--fallback-b2)); - border-color: var(--btn-color, var(--fallback-b2)); - } - } - - .btn.glass:hover { - --glass-opacity: 25%; - --glass-border-opacity: 15%; - } - - .btn-ghost:hover { - border-color: transparent; - } - - @supports (color: oklch(0% 0 0)) { - .btn-ghost:hover { - background-color: var(--fallback-bc,oklch(var(--bc)/0.2)); - } - } - - .btn-outline:hover { - --tw-border-opacity: 1; - border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity))); - --tw-bg-opacity: 1; - background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-b1,oklch(var(--b1)/var(--tw-text-opacity))); - } - - .btn-outline.btn-primary:hover { - --tw-text-opacity: 1; - color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); - } - - @supports (color: color-mix(in oklab, black, black)) { - .btn-outline.btn-primary:hover { - background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); - border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); - } - } - - .btn-outline.btn-secondary:hover { - --tw-text-opacity: 1; - color: var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity))); - } - - @supports (color: color-mix(in oklab, black, black)) { - .btn-outline.btn-secondary:hover { - background-color: color-mix(in oklab, var(--fallback-s,oklch(var(--s)/1)) 90%, black); - border-color: color-mix(in oklab, var(--fallback-s,oklch(var(--s)/1)) 90%, black); - } - } - - .btn-outline.btn-accent:hover { - --tw-text-opacity: 1; - color: var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity))); - } - - @supports (color: color-mix(in oklab, black, black)) { - .btn-outline.btn-accent:hover { - background-color: color-mix(in oklab, var(--fallback-a,oklch(var(--a)/1)) 90%, black); - border-color: color-mix(in oklab, var(--fallback-a,oklch(var(--a)/1)) 90%, black); - } - } - - .btn-outline.btn-success:hover { - --tw-text-opacity: 1; - color: var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity))); - } - - @supports (color: color-mix(in oklab, black, black)) { - .btn-outline.btn-success:hover { - background-color: color-mix(in oklab, var(--fallback-su,oklch(var(--su)/1)) 90%, black); - border-color: color-mix(in oklab, var(--fallback-su,oklch(var(--su)/1)) 90%, black); - } - } - - .btn-outline.btn-info:hover { - --tw-text-opacity: 1; - color: var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity))); - } - - @supports (color: color-mix(in oklab, black, black)) { - .btn-outline.btn-info:hover { - background-color: color-mix(in oklab, var(--fallback-in,oklch(var(--in)/1)) 90%, black); - border-color: color-mix(in oklab, var(--fallback-in,oklch(var(--in)/1)) 90%, black); - } - } - - .btn-outline.btn-warning:hover { - --tw-text-opacity: 1; - color: var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity))); - } - - @supports (color: color-mix(in oklab, black, black)) { - .btn-outline.btn-warning:hover { - background-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black); - border-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black); - } - } - - .btn-outline.btn-error:hover { - --tw-text-opacity: 1; - color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity))); - } - - @supports (color: color-mix(in oklab, black, black)) { - .btn-outline.btn-error:hover { - background-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black); - border-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black); - } - } - - .btn-disabled:hover, - .btn[disabled]:hover, - .btn:disabled:hover { - --tw-border-opacity: 0; - background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); - --tw-bg-opacity: 0.2; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); - --tw-text-opacity: 0.2; - } - - @supports (color: color-mix(in oklab, black, black)) { - .btn:is(input[type="checkbox"]:checked):hover, .btn:is(input[type="radio"]:checked):hover { - background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); - border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); - } - } - - .dropdown.dropdown-hover:hover .dropdown-content { - --tw-scale-x: 1; - --tw-scale-y: 1; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - } - - :where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(.active, .btn):hover, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(.active, .btn):hover { - cursor: pointer; - outline: 2px solid transparent; - outline-offset: 2px; - } - - @supports (color: oklch(0% 0 0)) { - :where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(.active, .btn):hover, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(.active, .btn):hover { - background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); - } - } - - .tab[disabled], - .tab[disabled]:hover { - cursor: not-allowed; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); - --tw-text-opacity: 0.2; - } -} - -.dropdown:is(details) summary::-webkit-details-marker { - display: none; -} - -.footer { - display: grid; - width: 100%; - grid-auto-flow: row; - place-items: start; - -moz-column-gap: 1rem; - column-gap: 1rem; - row-gap: 2.5rem; - font-size: 0.875rem; - line-height: 1.25rem; -} - -.footer > * { - display: grid; - place-items: start; - gap: 0.5rem; -} - -.footer-center { - place-items: center; - text-align: center; -} - -.footer-center > * { - place-items: center; -} - -@media (min-width: 48rem) { - .footer { - grid-auto-flow: column; - } - - .footer-center { - grid-auto-flow: row dense; - } -} - -.form-control { - display: flex; - flex-direction: column; -} - -.label { - display: flex; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - align-items: center; - justify-content: space-between; - padding-left: 0.25rem; - padding-right: 0.25rem; - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} - -.indicator { - position: relative; - display: inline-flex; - width: -moz-max-content; - width: max-content; -} - -.indicator :where(.indicator-item) { - z-index: 1; - position: absolute; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - white-space: nowrap; -} - -.input { - flex-shrink: 1; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - height: 3rem; - padding-left: 1rem; - padding-right: 1rem; - font-size: 1rem; - line-height: 2; - line-height: 1.5rem; - border-radius: var(--rounded-btn, 0.5rem); - border-width: 1px; - border-color: transparent; - --tw-bg-opacity: 1; - background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); -} - -.input[type="number"]::-webkit-inner-spin-button, -.input-md[type="number"]::-webkit-inner-spin-button { - margin-top: -1rem; - margin-bottom: -1rem; - margin-inline-end: -1rem; -} - -.join { - display: inline-flex; - align-items: stretch; - border-radius: var(--rounded-btn, 0.5rem); -} - -.join :where(.join-item) { - border-start-end-radius: 0; - border-end-end-radius: 0; - border-end-start-radius: 0; - border-start-start-radius: 0; -} - -.join .join-item:not(:first-child):not(:last-child), - .join *:not(:first-child):not(:last-child) .join-item { - border-start-end-radius: 0; - border-end-end-radius: 0; - border-end-start-radius: 0; - border-start-start-radius: 0; -} - -.join .join-item:first-child:not(:last-child), - .join *:first-child:not(:last-child) .join-item { - border-start-end-radius: 0; - border-end-end-radius: 0; -} - -.join .dropdown .join-item:first-child:not(:last-child), - .join *:first-child:not(:last-child) .dropdown .join-item { - border-start-end-radius: inherit; - border-end-end-radius: inherit; -} - -.join :where(.join-item:first-child:not(:last-child)), - .join :where(*:first-child:not(:last-child) .join-item) { - border-end-start-radius: inherit; - border-start-start-radius: inherit; -} - -.join .join-item:last-child:not(:first-child), - .join *:last-child:not(:first-child) .join-item { - border-end-start-radius: 0; - border-start-start-radius: 0; -} - -.join :where(.join-item:last-child:not(:first-child)), - .join :where(*:last-child:not(:first-child) .join-item) { - border-start-end-radius: inherit; - border-end-end-radius: inherit; -} - -@supports not selector(:has(*)) { - :where(.join *) { - border-radius: inherit; - } -} - -@supports selector(:has(*)) { - :where(.join *:has(.join-item)) { - border-radius: inherit; - } -} - -.link { - cursor: pointer; - text-decoration-line: underline; -} - -.menu { - display: flex; - flex-direction: column; - flex-wrap: wrap; - font-size: 0.875rem; - line-height: 1.25rem; - padding: 0.5rem; -} - -.menu :where(li ul) { - position: relative; - white-space: nowrap; - margin-inline-start: 1rem; - padding-inline-start: 0.5rem; -} - -.menu :where(li:not(.menu-title) > *:not(ul, details, .menu-title, .btn)), .menu :where(li:not(.menu-title) > details > summary:not(.menu-title)) { - display: grid; - grid-auto-flow: column; - align-content: flex-start; - align-items: center; - gap: 0.5rem; - grid-auto-columns: minmax(auto, max-content) auto max-content; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; -} - -.menu li.disabled { - cursor: not-allowed; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - color: var(--fallback-bc,oklch(var(--bc)/0.3)); -} - -.menu :where(li > .menu-dropdown:not(.menu-dropdown-show)) { - display: none; -} - -:where(.menu li) { - position: relative; - display: flex; - flex-shrink: 0; - flex-direction: column; - flex-wrap: wrap; - align-items: stretch; -} - -:where(.menu li) .badge { - justify-self: end; -} - -.modal { - pointer-events: none; - position: fixed; - inset: 0px; - margin: 0px; - display: grid; - height: 100%; - max-height: none; - width: 100%; - max-width: none; - justify-items: center; - padding: 0px; - opacity: 0; - overscroll-behavior: contain; - z-index: 999; - background-color: transparent; - color: inherit; - transition-duration: 200ms; - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); - transition-property: transform, opacity, visibility; - overflow-y: hidden; -} - -:where(.modal) { - align-items: center; -} - -.modal-box { - max-height: calc(100vh - 5em); - grid-column-start: 1; - grid-row-start: 1; - width: 91.666667%; - max-width: 32rem; - --tw-scale-x: .9; - --tw-scale-y: .9; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - border-bottom-right-radius: var(--rounded-box, 1rem); - border-bottom-left-radius: var(--rounded-box, 1rem); - border-top-left-radius: var(--rounded-box, 1rem); - border-top-right-radius: var(--rounded-box, 1rem); - --tw-bg-opacity: 1; - background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); - padding: 1.5rem; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); - transition-duration: 200ms; - box-shadow: rgba(0, 0, 0, 0.25) 0px 25px 50px -12px; - overflow-y: auto; - overscroll-behavior: contain; -} - -.modal-open, -.modal:target, -.modal-toggle:checked + .modal, -.modal[open] { - pointer-events: auto; - visibility: visible; - opacity: 1; -} - -:root:has(:is(.modal-open, .modal:target, .modal-toggle:checked + .modal, .modal[open])) { - overflow: hidden; - scrollbar-gutter: stable; -} - -.navbar { - display: flex; - align-items: center; - padding: var(--navbar-padding, 0.5rem); - min-height: 4rem; - width: 100%; -} - -:where(.navbar > *:not(script, style)) { - display: inline-flex; - align-items: center; -} - -.navbar-end { - width: 50%; - justify-content: flex-end; -} - -.progress { - position: relative; - width: 100%; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - overflow: hidden; - height: 0.5rem; - border-radius: var(--rounded-box, 1rem); - background-color: var(--fallback-bc,oklch(var(--bc)/0.2)); -} - -.radio { - flex-shrink: 0; - --chkbg: var(--bc); - height: 1.5rem; - width: 1.5rem; - cursor: pointer; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - border-radius: 9999px; - border-width: 1px; - border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity))); - --tw-border-opacity: 0.2; -} - -.range { - height: 1.5rem; - width: 100%; - cursor: pointer; - -moz-appearance: none; - appearance: none; - -webkit-appearance: none; - --range-shdw: var(--fallback-bc,oklch(var(--bc)/1)); - overflow: hidden; - border-radius: var(--rounded-box, 1rem); - background-color: transparent; -} - -.range:focus { - outline: none; -} - -.select { - display: inline-flex; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - height: 3rem; - min-height: 3rem; - padding-inline-start: 1rem; - padding-inline-end: 2.5rem; - font-size: 0.875rem; - line-height: 1.25rem; - line-height: 2; - border-radius: var(--rounded-btn, 0.5rem); - border-width: 1px; - border-color: transparent; - --tw-bg-opacity: 1; - background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); - background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), - linear-gradient(135deg, currentColor 50%, transparent 50%); - background-position: calc(100% - 20px) calc(1px + 50%), - calc(100% - 16.1px) calc(1px + 50%); - background-size: 4px 4px, - 4px 4px; - background-repeat: no-repeat; -} - -.select[multiple] { - height: auto; -} - -.steps { - display: inline-grid; - grid-auto-flow: column; - overflow: hidden; - overflow-x: auto; - counter-reset: step; - grid-auto-columns: 1fr; -} - -.steps .step { - display: grid; - grid-template-columns: repeat(1, minmax(0, 1fr)); - grid-template-columns: auto; - grid-template-rows: repeat(2, minmax(0, 1fr)); - grid-template-rows: 40px 1fr; - place-items: center; - text-align: center; - min-width: 4rem; -} - -.swap { - position: relative; - display: inline-grid; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - place-content: center; - cursor: pointer; -} - -.swap > * { - grid-column-start: 1; - grid-row-start: 1; - transition-duration: 300ms; - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); - transition-property: transform, opacity; -} - -.swap input { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} - -.swap .swap-on, -.swap .swap-indeterminate, -.swap input:indeterminate ~ .swap-on { - opacity: 0; -} - -.swap input:checked ~ .swap-off, -.swap-active .swap-off, -.swap input:indeterminate ~ .swap-off { - opacity: 0; -} - -.swap input:checked ~ .swap-on, -.swap-active .swap-on, -.swap input:indeterminate ~ .swap-indeterminate { - opacity: 1; -} - -.tabs { - display: grid; - align-items: flex-end; -} - -.tabs-lifted:has(.tab-content[class^="rounded-"]) - .tab:first-child:not(:is(.tab-active, [aria-selected="true"])), .tabs-lifted:has(.tab-content[class*=" rounded-"]) - .tab:first-child:not(:is(.tab-active, [aria-selected="true"])) { - border-bottom-color: transparent; -} - -.tab { - position: relative; - grid-row-start: 1; - display: inline-flex; - height: 2rem; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - flex-wrap: wrap; - align-items: center; - justify-content: center; - text-align: center; - font-size: 0.875rem; - line-height: 1.25rem; - line-height: 2; - --tab-padding: 1rem; - --tw-text-opacity: 0.5; - --tab-color: var(--fallback-bc,oklch(var(--bc)/1)); - --tab-bg: var(--fallback-b1,oklch(var(--b1)/1)); - --tab-border-color: var(--fallback-b3,oklch(var(--b3)/1)); - color: var(--tab-color); - padding-inline-start: var(--tab-padding, 1rem); - padding-inline-end: var(--tab-padding, 1rem); -} - -.tab:is(input[type="radio"]) { - width: auto; - border-bottom-right-radius: 0px; - border-bottom-left-radius: 0px; -} - -.tab:is(input[type="radio"]):after { - --tw-content: attr(aria-label); - content: var(--tw-content); -} - -.tab:not(input):empty { - cursor: default; - grid-column-start: span 9999; -} - -.tab-content { - grid-column-start: 1; - grid-column-end: span 9999; - grid-row-start: 2; - margin-top: calc(var(--tab-border) * -1); - display: none; - border-color: transparent; - border-width: var(--tab-border, 0); -} - -:checked + .tab-content:nth-child(2), - :is(.tab-active, [aria-selected="true"]) + .tab-content:nth-child(2) { - border-start-start-radius: 0px; -} - -input.tab:checked + .tab-content, -:is(.tab-active, [aria-selected="true"]) + .tab-content { - display: block; -} - -.table { - position: relative; - width: 100%; - border-radius: var(--rounded-box, 1rem); - text-align: left; - font-size: 0.875rem; - line-height: 1.25rem; -} - -.table :where(.table-pin-rows thead tr) { - position: sticky; - top: 0px; - z-index: 1; - --tw-bg-opacity: 1; - background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); -} - -.table :where(.table-pin-rows tfoot tr) { - position: sticky; - bottom: 0px; - z-index: 1; - --tw-bg-opacity: 1; - background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); -} - -.table :where(.table-pin-cols tr th) { - position: sticky; - left: 0px; - right: 0px; - --tw-bg-opacity: 1; - background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); -} - -.table-zebra tbody tr:nth-child(even) :where(.table-pin-cols tr th) { - --tw-bg-opacity: 1; - background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); -} - -.textarea { - min-height: 3rem; - flex-shrink: 1; - padding-left: 1rem; - padding-right: 1rem; - padding-top: 0.5rem; - padding-bottom: 0.5rem; - font-size: 0.875rem; - line-height: 1.25rem; - line-height: 2; - border-radius: var(--rounded-btn, 0.5rem); - border-width: 1px; - border-color: transparent; - --tw-bg-opacity: 1; - background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); -} - -.toggle { - flex-shrink: 0; - --tglbg: var(--fallback-b1,oklch(var(--b1)/1)); - --handleoffset: 1.5rem; - --handleoffsetcalculator: calc(var(--handleoffset) * -1); - --togglehandleborder: 0 0; - height: 1.5rem; - width: 3rem; - cursor: pointer; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - border-radius: var(--rounded-badge, 1.9rem); - border-width: 1px; - border-color: currentColor; - background-color: currentColor; - color: var(--fallback-bc,oklch(var(--bc)/0.5)); - transition: background, - box-shadow var(--animation-input, 0.2s) ease-out; - box-shadow: var(--handleoffsetcalculator) 0 0 2px var(--tglbg) inset, - 0 0 0 2px var(--tglbg) inset, - var(--togglehandleborder); -} - -.alert-info { - border-color: var(--fallback-in,oklch(var(--in)/0.2)); - --tw-text-opacity: 1; - color: var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity))); - --alert-bg: var(--fallback-in,oklch(var(--in)/1)); - --alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1)); -} - -.alert-success { - border-color: var(--fallback-su,oklch(var(--su)/0.2)); - --tw-text-opacity: 1; - color: var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity))); - --alert-bg: var(--fallback-su,oklch(var(--su)/1)); - --alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1)); -} - -.alert-warning { - border-color: var(--fallback-wa,oklch(var(--wa)/0.2)); - --tw-text-opacity: 1; - color: var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity))); - --alert-bg: var(--fallback-wa,oklch(var(--wa)/1)); - --alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1)); -} - -.alert-error { - border-color: var(--fallback-er,oklch(var(--er)/0.2)); - --tw-text-opacity: 1; - color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity))); - --alert-bg: var(--fallback-er,oklch(var(--er)/1)); - --alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1)); -} - -.avatar-group :where(.avatar) { - overflow: hidden; - border-radius: 9999px; - border-width: 4px; - --tw-border-opacity: 1; - border-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-border-opacity))); -} - -.badge-neutral { - --tw-border-opacity: 1; - border-color: var(--fallback-n,oklch(var(--n)/var(--tw-border-opacity))); - --tw-bg-opacity: 1; - background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); -} - -.badge-primary { - --tw-border-opacity: 1; - border-color: var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity))); - --tw-bg-opacity: 1; - background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); -} - -.badge-ghost { - --tw-border-opacity: 1; - border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); - --tw-bg-opacity: 1; - background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); -} - -.badge-outline.badge-neutral { - --tw-text-opacity: 1; - color: var(--fallback-n,oklch(var(--n)/var(--tw-text-opacity))); -} - -.badge-outline.badge-primary { - --tw-text-opacity: 1; - color: var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity))); -} - -.btm-nav > *.disabled, - .btm-nav > *[disabled] { - pointer-events: none; - --tw-border-opacity: 0; - background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); - --tw-bg-opacity: 0.1; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); - --tw-text-opacity: 0.2; -} - -.btm-nav > * .label { - font-size: 1rem; - line-height: 1.5rem; -} - -@media (prefers-reduced-motion: no-preference) { - .btn { - animation: button-pop var(--animation-btn, 0.25s) ease-out; - } -} - -.btn:active:hover, - .btn:active:focus { - animation: button-pop 0s ease-out; - transform: scale(var(--btn-focus-scale, 0.97)); -} - -@supports not (color: oklch(0% 0 0)) { - .btn { - background-color: var(--btn-color, var(--fallback-b2)); - border-color: var(--btn-color, var(--fallback-b2)); - } - - .btn-primary { - --btn-color: var(--fallback-p); - } - - .btn-neutral { - --btn-color: var(--fallback-n); - } -} - -@supports (color: color-mix(in oklab, black, black)) { - .btn-outline.btn-primary.btn-active { - background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); - border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); - } - - .btn-outline.btn-secondary.btn-active { - background-color: color-mix(in oklab, var(--fallback-s,oklch(var(--s)/1)) 90%, black); - border-color: color-mix(in oklab, var(--fallback-s,oklch(var(--s)/1)) 90%, black); - } - - .btn-outline.btn-accent.btn-active { - background-color: color-mix(in oklab, var(--fallback-a,oklch(var(--a)/1)) 90%, black); - border-color: color-mix(in oklab, var(--fallback-a,oklch(var(--a)/1)) 90%, black); - } - - .btn-outline.btn-success.btn-active { - background-color: color-mix(in oklab, var(--fallback-su,oklch(var(--su)/1)) 90%, black); - border-color: color-mix(in oklab, var(--fallback-su,oklch(var(--su)/1)) 90%, black); - } - - .btn-outline.btn-info.btn-active { - background-color: color-mix(in oklab, var(--fallback-in,oklch(var(--in)/1)) 90%, black); - border-color: color-mix(in oklab, var(--fallback-in,oklch(var(--in)/1)) 90%, black); - } - - .btn-outline.btn-warning.btn-active { - background-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black); - border-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black); - } - - .btn-outline.btn-error.btn-active { - background-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black); - border-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black); - } -} - -.btn:focus-visible { - outline-style: solid; - outline-width: 2px; - outline-offset: 2px; -} - -.btn-primary { - --tw-text-opacity: 1; - color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); - outline-color: var(--fallback-p,oklch(var(--p)/1)); -} - -@supports (color: oklch(0% 0 0)) { - .btn-primary { - --btn-color: var(--p); - } - - .btn-neutral { - --btn-color: var(--n); - } -} - -.btn-neutral { - --tw-text-opacity: 1; - color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); - outline-color: var(--fallback-n,oklch(var(--n)/1)); -} - -.btn.glass { - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); - outline-color: currentColor; -} - -.btn.glass.btn-active { - --glass-opacity: 25%; - --glass-border-opacity: 15%; -} - -.btn-ghost { - border-width: 1px; - border-color: transparent; - background-color: transparent; - color: currentColor; - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); - outline-color: currentColor; -} - -.btn-ghost.btn-active { - border-color: transparent; - background-color: var(--fallback-bc,oklch(var(--bc)/0.2)); -} - -.btn-outline { - border-color: currentColor; - background-color: transparent; - --tw-text-opacity: 1; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.btn-outline.btn-active { - --tw-border-opacity: 1; - border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity))); - --tw-bg-opacity: 1; - background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-b1,oklch(var(--b1)/var(--tw-text-opacity))); -} - -.btn-outline.btn-primary { - --tw-text-opacity: 1; - color: var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity))); -} - -.btn-outline.btn-primary.btn-active { - --tw-text-opacity: 1; - color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); -} - -.btn-outline.btn-secondary { - --tw-text-opacity: 1; - color: var(--fallback-s,oklch(var(--s)/var(--tw-text-opacity))); -} - -.btn-outline.btn-secondary.btn-active { - --tw-text-opacity: 1; - color: var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity))); -} - -.btn-outline.btn-accent { - --tw-text-opacity: 1; - color: var(--fallback-a,oklch(var(--a)/var(--tw-text-opacity))); -} - -.btn-outline.btn-accent.btn-active { - --tw-text-opacity: 1; - color: var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity))); -} - -.btn-outline.btn-success { - --tw-text-opacity: 1; - color: var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity))); -} - -.btn-outline.btn-success.btn-active { - --tw-text-opacity: 1; - color: var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity))); -} - -.btn-outline.btn-info { - --tw-text-opacity: 1; - color: var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity))); -} - -.btn-outline.btn-info.btn-active { - --tw-text-opacity: 1; - color: var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity))); -} - -.btn-outline.btn-warning { - --tw-text-opacity: 1; - color: var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity))); -} - -.btn-outline.btn-warning.btn-active { - --tw-text-opacity: 1; - color: var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity))); -} - -.btn-outline.btn-error { - --tw-text-opacity: 1; - color: var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity))); -} - -.btn-outline.btn-error.btn-active { - --tw-text-opacity: 1; - color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity))); -} - -.btn.btn-disabled, - .btn[disabled], - .btn:disabled { - --tw-border-opacity: 0; - background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); - --tw-bg-opacity: 0.2; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); - --tw-text-opacity: 0.2; -} - -.btn:is(input[type="checkbox"]:checked), -.btn:is(input[type="radio"]:checked) { - --tw-border-opacity: 1; - border-color: var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity))); - --tw-bg-opacity: 1; - background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); -} - -.btn:is(input[type="checkbox"]:checked):focus-visible, .btn:is(input[type="radio"]:checked):focus-visible { - outline-color: var(--fallback-p,oklch(var(--p)/1)); -} - -@keyframes button-pop { - 0% { - transform: scale(var(--btn-focus-scale, 0.98)); - } - - 40% { - transform: scale(1.02); - } - - 100% { - transform: scale(1); - } -} - -.card :where(figure:first-child) { - overflow: hidden; - border-start-start-radius: inherit; - border-start-end-radius: inherit; - border-end-start-radius: unset; - border-end-end-radius: unset; -} - -.card :where(figure:last-child) { - overflow: hidden; - border-start-start-radius: unset; - border-start-end-radius: unset; - border-end-start-radius: inherit; - border-end-end-radius: inherit; -} - -.card:focus-visible { - outline: 2px solid currentColor; - outline-offset: 2px; -} - -.card.bordered { - border-width: 1px; - --tw-border-opacity: 1; - border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); -} - -.card.compact .card-body { - padding: 1rem; - font-size: 0.875rem; - line-height: 1.25rem; -} - -.card-title { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 1.25rem; - line-height: 1.75rem; - font-weight: 600; -} - -.card.image-full :where(figure) { - overflow: hidden; - border-radius: inherit; -} - -.carousel::-webkit-scrollbar { - display: none; -} - -.checkbox:focus { - box-shadow: none; -} - -.checkbox:focus-visible { - outline-style: solid; - outline-width: 2px; - outline-offset: 2px; - outline-color: var(--fallback-bc,oklch(var(--bc)/1)); -} - -.checkbox:disabled { - border-width: 0px; - cursor: not-allowed; - border-color: transparent; - --tw-bg-opacity: 1; - background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); - opacity: 0.2; -} - -.checkbox:checked, - .checkbox[aria-checked="true"] { - background-repeat: no-repeat; - animation: checkmark var(--animation-input, 0.2s) ease-out; - background-color: var(--chkbg); - background-image: linear-gradient(-45deg, transparent 65%, var(--chkbg) 65.99%), - linear-gradient(45deg, transparent 75%, var(--chkbg) 75.99%), - linear-gradient(-45deg, var(--chkbg) 40%, transparent 40.99%), - linear-gradient( - 45deg, - var(--chkbg) 30%, - var(--chkfg) 30.99%, - var(--chkfg) 40%, - transparent 40.99% - ), - linear-gradient(-45deg, var(--chkfg) 50%, var(--chkbg) 50.99%); -} - -.checkbox:indeterminate { - --tw-bg-opacity: 1; - background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); - background-repeat: no-repeat; - animation: checkmark var(--animation-input, 0.2s) ease-out; - background-image: linear-gradient(90deg, transparent 80%, var(--chkbg) 80%), - linear-gradient(-90deg, transparent 80%, var(--chkbg) 80%), - linear-gradient(0deg, var(--chkbg) 43%, var(--chkfg) 43%, var(--chkfg) 57%, var(--chkbg) 57%); -} - -@keyframes checkmark { - 0% { - background-position-y: 5px; - } - - 50% { - background-position-y: -2px; - } - - 100% { - background-position-y: 0; - } -} - -details.collapse { - width: 100%; -} - -details.collapse summary { - position: relative; - display: block; - outline: 2px solid transparent; - outline-offset: 2px; -} - -details.collapse summary::-webkit-details-marker { - display: none; -} - -.collapse:focus-visible { - outline-style: solid; - outline-width: 2px; - outline-offset: 2px; - outline-color: var(--fallback-bc,oklch(var(--bc)/1)); -} - -.collapse:has(.collapse-title:focus-visible), -.collapse:has(> input[type="checkbox"]:focus-visible), -.collapse:has(> input[type="radio"]:focus-visible) { - outline-style: solid; - outline-width: 2px; - outline-offset: 2px; - outline-color: var(--fallback-bc,oklch(var(--bc)/1)); -} - -.collapse-arrow > .collapse-title:after { - position: absolute; - display: block; - height: 0.5rem; - width: 0.5rem; - --tw-translate-y: -100%; - --tw-rotate: 45deg; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - transition-property: all; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); - transition-duration: 150ms; - transition-duration: 0.2s; - top: 1.9rem; - inset-inline-end: 1.4rem; - content: ""; - transform-origin: 75% 75%; - box-shadow: 2px 2px; - pointer-events: none; -} - -.collapse-plus > .collapse-title:after { - position: absolute; - display: block; - height: 0.5rem; - width: 0.5rem; - transition-property: all; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); - transition-duration: 300ms; - top: 0.9rem; - inset-inline-end: 1.4rem; - content: "+"; - pointer-events: none; -} - -.collapse:not(.collapse-open):not(.collapse-close) > input[type="checkbox"], -.collapse:not(.collapse-open):not(.collapse-close) > input[type="radio"]:not(:checked), -.collapse:not(.collapse-open):not(.collapse-close) > .collapse-title { - cursor: pointer; -} - -.collapse:focus:not(.collapse-open):not(.collapse-close):not(.collapse[open]) > .collapse-title { - cursor: unset; -} - -.collapse-title { - position: relative; -} - -:where(.collapse > input[type="checkbox"]), -:where(.collapse > input[type="radio"]) { - z-index: 1; -} - -.collapse-title, -:where(.collapse > input[type="checkbox"]), -:where(.collapse > input[type="radio"]) { - width: 100%; - padding: 1rem; - padding-inline-end: 3rem; - min-height: 3.75rem; - transition: background-color 0.2s ease-out; -} - -.collapse[open] > :where(.collapse-content), -.collapse-open > :where(.collapse-content), -.collapse:focus:not(.collapse-close) > :where(.collapse-content), -.collapse:not(.collapse-close) > :where(input[type="checkbox"]:checked ~ .collapse-content), -.collapse:not(.collapse-close) > :where(input[type="radio"]:checked ~ .collapse-content) { - padding-bottom: 1rem; - transition: padding 0.2s ease-out, - background-color 0.2s ease-out; -} - -.collapse[open].collapse-arrow > .collapse-title:after, -.collapse-open.collapse-arrow > .collapse-title:after, -.collapse-arrow:focus:not(.collapse-close) > .collapse-title:after, -.collapse-arrow:not(.collapse-close) > input[type="checkbox"]:checked ~ .collapse-title:after, -.collapse-arrow:not(.collapse-close) > input[type="radio"]:checked ~ .collapse-title:after { - --tw-translate-y: -50%; - --tw-rotate: 225deg; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.collapse[open].collapse-plus > .collapse-title:after, -.collapse-open.collapse-plus > .collapse-title:after, -.collapse-plus:focus:not(.collapse-close) > .collapse-title:after, -.collapse-plus:not(.collapse-close) > input[type="checkbox"]:checked ~ .collapse-title:after, -.collapse-plus:not(.collapse-close) > input[type="radio"]:checked ~ .collapse-title:after { - content: "−"; -} - -.divider:not(:empty) { - gap: 1rem; -} - -.drawer-toggle:checked ~ .drawer-side > .drawer-overlay { - background-color: #0006; -} - -.drawer-toggle:focus-visible ~ .drawer-content label.drawer-button { - outline-style: solid; - outline-width: 2px; - outline-offset: 2px; -} - -.dropdown.dropdown-open .dropdown-content, -.dropdown:focus .dropdown-content, -.dropdown:focus-within .dropdown-content { - --tw-scale-x: 1; - --tw-scale-y: 1; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.label-text { - font-size: 0.875rem; - line-height: 1.25rem; - --tw-text-opacity: 1; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); -} - -.input input { - --tw-bg-opacity: 1; - background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); - background-color: transparent; -} - -.input input:focus { - outline: 2px solid transparent; - outline-offset: 2px; -} - -.input[list]::-webkit-calendar-picker-indicator { - line-height: 1em; -} - -.input-bordered { - border-color: var(--fallback-bc,oklch(var(--bc)/0.2)); -} - -.input:focus, - .input:focus-within { - box-shadow: none; - border-color: var(--fallback-bc,oklch(var(--bc)/0.2)); - outline-style: solid; - outline-width: 2px; - outline-offset: 2px; - outline-color: var(--fallback-bc,oklch(var(--bc)/0.2)); -} - -.input-primary { - --tw-border-opacity: 1; - border-color: var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity))); -} - -.input-primary:focus, - .input-primary:focus-within { - --tw-border-opacity: 1; - border-color: var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity))); - outline-color: var(--fallback-p,oklch(var(--p)/1)); -} - -.input:has(> input[disabled]), - .input-disabled, - .input:disabled, - .input[disabled] { - cursor: not-allowed; - --tw-border-opacity: 1; - border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); - --tw-bg-opacity: 1; - background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); - color: var(--fallback-bc,oklch(var(--bc)/0.4)); -} - -.input:has(> input[disabled])::-moz-placeholder, .input-disabled::-moz-placeholder, .input:disabled::-moz-placeholder, .input[disabled]::-moz-placeholder { - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); - --tw-placeholder-opacity: 0.2; -} - -.input:has(> input[disabled])::placeholder, - .input-disabled::placeholder, - .input:disabled::placeholder, - .input[disabled]::placeholder { - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); - --tw-placeholder-opacity: 0.2; -} - -.input:has(> input[disabled]) > input[disabled] { - cursor: not-allowed; -} - -.input::-webkit-date-and-time-value { - text-align: inherit; -} - -.join > :where(*:not(:first-child)) { - margin-top: 0px; - margin-bottom: 0px; - margin-inline-start: -1px; -} - -.join > :where(*:not(:first-child)):is(.btn) { - margin-inline-start: calc(var(--border-btn) * -1); -} - -.link-primary { - --tw-text-opacity: 1; - color: var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity))); -} - -@supports (color:color-mix(in oklab,black,black)) { - @media (hover:hover) { - .link-primary:hover { - color: color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 80%,black); - } - } -} - -.link:focus { - outline: 2px solid transparent; - outline-offset: 2px; -} - -.link:focus-visible { - outline: 2px solid currentColor; - outline-offset: 2px; -} - -.loading { - pointer-events: none; - display: inline-block; - aspect-ratio: 1 / 1; - width: 1.5rem; - background-color: currentColor; - -webkit-mask-size: 100%; - mask-size: 100%; - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-position: center; - mask-position: center; - -webkit-mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_V8m1%7Btransform-origin:center;animation:spinner_zKoa 2s linear infinite%7D.spinner_V8m1 circle%7Bstroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite%7D%40keyframes spinner_zKoa%7B100%25%7Btransform:rotate(360deg)%7D%7D%40keyframes spinner_YpZS%7B0%25%7Bstroke-dasharray:0 150;stroke-dashoffset:0%7D47.5%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-16%7D95%25%2C100%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-59%7D%7D%3C%2Fstyle%3E%3Cg class='spinner_V8m1'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3E%3C%2Fcircle%3E%3C%2Fg%3E%3C%2Fsvg%3E"); - mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_V8m1%7Btransform-origin:center;animation:spinner_zKoa 2s linear infinite%7D.spinner_V8m1 circle%7Bstroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite%7D%40keyframes spinner_zKoa%7B100%25%7Btransform:rotate(360deg)%7D%7D%40keyframes spinner_YpZS%7B0%25%7Bstroke-dasharray:0 150;stroke-dashoffset:0%7D47.5%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-16%7D95%25%2C100%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-59%7D%7D%3C%2Fstyle%3E%3Cg class='spinner_V8m1'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3E%3C%2Fcircle%3E%3C%2Fg%3E%3C%2Fsvg%3E"); -} - -.loading-spinner { - -webkit-mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_V8m1%7Btransform-origin:center;animation:spinner_zKoa 2s linear infinite%7D.spinner_V8m1 circle%7Bstroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite%7D%40keyframes spinner_zKoa%7B100%25%7Btransform:rotate(360deg)%7D%7D%40keyframes spinner_YpZS%7B0%25%7Bstroke-dasharray:0 150;stroke-dashoffset:0%7D47.5%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-16%7D95%25%2C100%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-59%7D%7D%3C%2Fstyle%3E%3Cg class='spinner_V8m1'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3E%3C%2Fcircle%3E%3C%2Fg%3E%3C%2Fsvg%3E"); - mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_V8m1%7Btransform-origin:center;animation:spinner_zKoa 2s linear infinite%7D.spinner_V8m1 circle%7Bstroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite%7D%40keyframes spinner_zKoa%7B100%25%7Btransform:rotate(360deg)%7D%7D%40keyframes spinner_YpZS%7B0%25%7Bstroke-dasharray:0 150;stroke-dashoffset:0%7D47.5%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-16%7D95%25%2C100%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-59%7D%7D%3C%2Fstyle%3E%3Cg class='spinner_V8m1'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3E%3C%2Fcircle%3E%3C%2Fg%3E%3C%2Fsvg%3E"); -} - -.loading-ball { - -webkit-mask-image: url("data:image/svg+xml,%0A%3Csvg width='24' height='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_rXNP%7Banimation:spinner_YeBj .8s infinite%7D@keyframes spinner_YeBj%7B0%25%7Banimation-timing-function:cubic-bezier(0.33,0,.66,.33);cy:5px%7D46.875%25%7Bcy:20px;rx:4px;ry:4px%7D50%25%7Banimation-timing-function:cubic-bezier(0.33,.66,.66,1);cy:20.5px;rx:4.8px;ry:3px%7D53.125%25%7Brx:4px;ry:4px%7D100%25%7Bcy:5px%7D%7D%3C/style%3E%3Cellipse class='spinner_rXNP' cx='12' cy='5' rx='4' ry='4'/%3E%3C/svg%3E"); - mask-image: url("data:image/svg+xml,%0A%3Csvg width='24' height='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_rXNP%7Banimation:spinner_YeBj .8s infinite%7D@keyframes spinner_YeBj%7B0%25%7Banimation-timing-function:cubic-bezier(0.33,0,.66,.33);cy:5px%7D46.875%25%7Bcy:20px;rx:4px;ry:4px%7D50%25%7Banimation-timing-function:cubic-bezier(0.33,.66,.66,1);cy:20.5px;rx:4.8px;ry:3px%7D53.125%25%7Brx:4px;ry:4px%7D100%25%7Bcy:5px%7D%7D%3C/style%3E%3Cellipse class='spinner_rXNP' cx='12' cy='5' rx='4' ry='4'/%3E%3C/svg%3E"); -} - -.loading-sm { - width: 1.25rem; -} - -.loading-md { - width: 1.5rem; -} - -.loading-lg { - width: 2.5rem; -} - -:where(.menu li:empty) { - --tw-bg-opacity: 1; - background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); - opacity: 0.1; - margin: 0.5rem 1rem; - height: 1px; -} - -.menu :where(li ul):before { - position: absolute; - bottom: 0.75rem; - inset-inline-start: 0px; - top: 0.75rem; - width: 1px; - --tw-bg-opacity: 1; - background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); - opacity: 0.1; - content: ""; -} - -.menu :where(li:not(.menu-title) > *:not(ul, details, .menu-title, .btn)), -.menu :where(li:not(.menu-title) > details > summary:not(.menu-title)) { - border-radius: var(--rounded-btn, 0.5rem); - padding-left: 1rem; - padding-right: 1rem; - padding-top: 0.5rem; - padding-bottom: 0.5rem; - text-align: start; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); - transition-duration: 200ms; - text-wrap: balance; -} - -:where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(summary, .active, .btn).focus, :where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(summary, .active, .btn):focus, :where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):is(summary):not(.active, .btn):focus-visible, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(summary, .active, .btn).focus, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(summary, .active, .btn):focus, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):is(summary):not(.active, .btn):focus-visible { - cursor: pointer; - background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); - --tw-text-opacity: 1; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); - outline: 2px solid transparent; - outline-offset: 2px; -} - -.menu li > *:not(ul, .menu-title, details, .btn):active, -.menu li > *:not(ul, .menu-title, details, .btn).active, -.menu li > details > summary:active { - --tw-bg-opacity: 1; - background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); -} - -.menu :where(li > details > summary)::-webkit-details-marker { - display: none; -} - -.menu :where(li > details > summary):after, -.menu :where(li > .menu-dropdown-toggle):after { - justify-self: end; - display: block; - margin-top: -0.5rem; - height: 0.5rem; - width: 0.5rem; - transform: rotate(45deg); - transition-property: transform, margin-top; - transition-duration: 0.3s; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - content: ""; - transform-origin: 75% 75%; - box-shadow: 2px 2px; - pointer-events: none; -} - -.menu :where(li > details[open] > summary):after, -.menu :where(li > .menu-dropdown-toggle.menu-dropdown-show):after { - transform: rotate(225deg); - margin-top: 0; -} - -.mockup-phone .display { - overflow: hidden; - border-radius: 40px; - margin-top: -25px; -} - -.mockup-browser .mockup-browser-toolbar .input { - position: relative; - margin-left: auto; - margin-right: auto; - display: block; - height: 1.75rem; - width: 24rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - --tw-bg-opacity: 1; - background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); - padding-left: 2rem; - direction: ltr; -} - -.mockup-browser .mockup-browser-toolbar .input:before { - content: ""; - position: absolute; - left: 0.5rem; - top: 50%; - aspect-ratio: 1 / 1; - height: 0.75rem; - --tw-translate-y: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - border-radius: 9999px; - border-width: 2px; - border-color: currentColor; - opacity: 0.6; -} - -.mockup-browser .mockup-browser-toolbar .input:after { - content: ""; - position: absolute; - left: 1.25rem; - top: 50%; - height: 0.5rem; - --tw-translate-y: 25%; - --tw-rotate: -45deg; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - border-radius: 9999px; - border-width: 1px; - border-color: currentColor; - opacity: 0.6; -} - -.modal:not(dialog:not(.modal-open)), - .modal::backdrop { - background-color: #0006; - animation: modal-pop 0.2s ease-out; -} - -.modal-backdrop { - z-index: -1; - grid-column-start: 1; - grid-row-start: 1; - display: grid; - align-self: stretch; - justify-self: stretch; - color: transparent; -} - -.modal-open .modal-box, -.modal-toggle:checked + .modal .modal-box, -.modal:target .modal-box, -.modal[open] .modal-box { - --tw-translate-y: 0px; - --tw-scale-x: 1; - --tw-scale-y: 1; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -@keyframes modal-pop { - 0% { - opacity: 0; - } -} - -.progress::-moz-progress-bar { - border-radius: var(--rounded-box, 1rem); - --tw-bg-opacity: 1; - background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); -} - -.progress:indeterminate { - --progress-color: var(--fallback-bc,oklch(var(--bc)/1)); - background-image: repeating-linear-gradient( - 90deg, - var(--progress-color) -1%, - var(--progress-color) 10%, - transparent 10%, - transparent 90% - ); - background-size: 200%; - background-position-x: 15%; - animation: progress-loading 5s ease-in-out infinite; -} - -.progress::-webkit-progress-bar { - border-radius: var(--rounded-box, 1rem); - background-color: transparent; -} - -.progress::-webkit-progress-value { - border-radius: var(--rounded-box, 1rem); - --tw-bg-opacity: 1; - background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); -} - -.progress:indeterminate::-moz-progress-bar { - background-color: transparent; - background-image: repeating-linear-gradient( - 90deg, - var(--progress-color) -1%, - var(--progress-color) 10%, - transparent 10%, - transparent 90% - ); - background-size: 200%; - background-position-x: 15%; - animation: progress-loading 5s ease-in-out infinite; -} - -@keyframes progress-loading { - 50% { - background-position-x: -115%; - } -} - -.radio:focus { - box-shadow: none; -} - -.radio:focus-visible { - outline-style: solid; - outline-width: 2px; - outline-offset: 2px; - outline-color: var(--fallback-bc,oklch(var(--bc)/1)); -} - -.radio:checked, - .radio[aria-checked="true"] { - --tw-bg-opacity: 1; - background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); - background-image: none; - animation: radiomark var(--animation-input, 0.2s) ease-out; - box-shadow: 0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset, - 0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset; -} - -.radio:disabled { - cursor: not-allowed; - opacity: 0.2; -} - -@keyframes radiomark { - 0% { - box-shadow: 0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset, - 0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset; - } - - 50% { - box-shadow: 0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset, - 0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset; - } - - 100% { - box-shadow: 0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset, - 0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset; - } -} - -.range:focus-visible::-webkit-slider-thumb { - --focus-shadow: 0 0 0 6px var(--fallback-b1,oklch(var(--b1)/1)) inset, 0 0 0 2rem var(--range-shdw) inset; -} - -.range:focus-visible::-moz-range-thumb { - --focus-shadow: 0 0 0 6px var(--fallback-b1,oklch(var(--b1)/1)) inset, 0 0 0 2rem var(--range-shdw) inset; -} - -.range::-webkit-slider-runnable-track { - height: 0.5rem; - width: 100%; - border-radius: var(--rounded-box, 1rem); - background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); -} - -.range::-moz-range-track { - height: 0.5rem; - width: 100%; - border-radius: var(--rounded-box, 1rem); - background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); -} - -.range::-webkit-slider-thumb { - position: relative; - height: 1.5rem; - width: 1.5rem; - border-radius: var(--rounded-box, 1rem); - border-style: none; - --tw-bg-opacity: 1; - background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); - appearance: none; - -webkit-appearance: none; - top: 50%; - color: var(--range-shdw); - transform: translateY(-50%); - --filler-size: 100rem; - --filler-offset: 0.6rem; - box-shadow: 0 0 0 3px var(--range-shdw) inset, - var(--focus-shadow, 0 0), - calc(var(--filler-size) * -1 - var(--filler-offset)) 0 0 var(--filler-size); -} - -.range::-moz-range-thumb { - position: relative; - height: 1.5rem; - width: 1.5rem; - border-radius: var(--rounded-box, 1rem); - border-style: none; - --tw-bg-opacity: 1; - background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); - top: 50%; - color: var(--range-shdw); - --filler-size: 100rem; - --filler-offset: 0.5rem; - box-shadow: 0 0 0 3px var(--range-shdw) inset, - var(--focus-shadow, 0 0), - calc(var(--filler-size) * -1 - var(--filler-offset)) 0 0 var(--filler-size); -} - -@keyframes rating-pop { - 0% { - transform: translateY(-0.125em); - } - - 40% { - transform: translateY(-0.125em); - } - - 100% { - transform: translateY(0); - } -} - -.select-bordered { - border-color: var(--fallback-bc,oklch(var(--bc)/0.2)); -} - -.select:focus { - box-shadow: none; - border-color: var(--fallback-bc,oklch(var(--bc)/0.2)); - outline-style: solid; - outline-width: 2px; - outline-offset: 2px; - outline-color: var(--fallback-bc,oklch(var(--bc)/0.2)); -} - -.select-disabled, - .select:disabled, - .select[disabled] { - cursor: not-allowed; - --tw-border-opacity: 1; - border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); - --tw-bg-opacity: 1; - background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); - color: var(--fallback-bc,oklch(var(--bc)/0.4)); -} - -.select-disabled::-moz-placeholder, .select:disabled::-moz-placeholder, .select[disabled]::-moz-placeholder { - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); - --tw-placeholder-opacity: 0.2; -} - -.select-disabled::placeholder, - .select:disabled::placeholder, - .select[disabled]::placeholder { - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); - --tw-placeholder-opacity: 0.2; -} - -.select-multiple, - .select[multiple], - .select[size].select:not([size="1"]) { - background-image: none; - padding-right: 1rem; -} - -[dir="rtl"] .select { - background-position: calc(0% + 12px) calc(1px + 50%), - calc(0% + 16px) calc(1px + 50%); -} - -.skeleton { - border-radius: var(--rounded-box, 1rem); - --tw-bg-opacity: 1; - background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity))); - will-change: background-position; - animation: skeleton 1.8s ease-in-out infinite; - background-image: linear-gradient( - 105deg, - transparent 0%, - transparent 40%, - var(--fallback-b1,oklch(var(--b1)/1)) 50%, - transparent 60%, - transparent 100% - ); - background-size: 200% auto; - background-repeat: no-repeat; - background-position-x: -50%; -} - -@media (prefers-reduced-motion) { - .skeleton { - animation-duration: 15s; - } -} - -@keyframes skeleton { - from { - background-position: 150%; - } - - to { - background-position: -50%; - } -} - -.steps .step:before { - top: 0px; - grid-column-start: 1; - grid-row-start: 1; - height: 0.5rem; - width: 100%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - --tw-bg-opacity: 1; - background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); - content: ""; - margin-inline-start: -100%; -} - -.steps .step:after { - content: counter(step); - counter-increment: step; - z-index: 1; - position: relative; - grid-column-start: 1; - grid-row-start: 1; - display: grid; - height: 2rem; - width: 2rem; - place-items: center; - place-self: center; - border-radius: 9999px; - --tw-bg-opacity: 1; - background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); -} - -.steps .step:first-child:before { - content: none; -} - -.steps .step[data-content]:after { - content: attr(data-content); -} - -.steps .step-neutral + .step-neutral:before, - .steps .step-neutral:after { - --tw-bg-opacity: 1; - background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); -} - -.steps .step-primary + .step-primary:before, - .steps .step-primary:after { - --tw-bg-opacity: 1; - background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); -} - -.steps .step-secondary + .step-secondary:before, - .steps .step-secondary:after { - --tw-bg-opacity: 1; - background-color: var(--fallback-s,oklch(var(--s)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity))); -} - -.steps .step-accent + .step-accent:before, - .steps .step-accent:after { - --tw-bg-opacity: 1; - background-color: var(--fallback-a,oklch(var(--a)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity))); -} - -.steps .step-info + .step-info:before { - --tw-bg-opacity: 1; - background-color: var(--fallback-in,oklch(var(--in)/var(--tw-bg-opacity))); -} - -.steps .step-info:after { - --tw-bg-opacity: 1; - background-color: var(--fallback-in,oklch(var(--in)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity))); -} - -.steps .step-success + .step-success:before { - --tw-bg-opacity: 1; - background-color: var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity))); -} - -.steps .step-success:after { - --tw-bg-opacity: 1; - background-color: var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity))); -} - -.steps .step-warning + .step-warning:before { - --tw-bg-opacity: 1; - background-color: var(--fallback-wa,oklch(var(--wa)/var(--tw-bg-opacity))); -} - -.steps .step-warning:after { - --tw-bg-opacity: 1; - background-color: var(--fallback-wa,oklch(var(--wa)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity))); -} - -.steps .step-error + .step-error:before { - --tw-bg-opacity: 1; - background-color: var(--fallback-er,oklch(var(--er)/var(--tw-bg-opacity))); -} - -.steps .step-error:after { - --tw-bg-opacity: 1; - background-color: var(--fallback-er,oklch(var(--er)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity))); -} - -.swap-rotate .swap-on, -.swap-rotate .swap-indeterminate, -.swap-rotate input:indeterminate ~ .swap-on { - --tw-rotate: 45deg; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.swap-rotate input:checked ~ .swap-off, -.swap-active:where(.swap-rotate) .swap-off, -.swap-rotate input:indeterminate ~ .swap-off { - --tw-rotate: -45deg; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.swap-rotate input:checked ~ .swap-on, -.swap-active:where(.swap-rotate) .swap-on, -.swap-rotate input:indeterminate ~ .swap-indeterminate { - --tw-rotate: 0deg; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.swap-flip .swap-on, -.swap-flip .swap-indeterminate, -.swap-flip input:indeterminate ~ .swap-on { - transform: rotateY(180deg); - backface-visibility: hidden; - opacity: 1; -} - -.swap-flip input:checked ~ .swap-off, -.swap-active:where(.swap-flip) .swap-off, -.swap-flip input:indeterminate ~ .swap-off { - transform: rotateY(-180deg); - backface-visibility: hidden; - opacity: 1; -} - -.swap-flip input:checked ~ .swap-on, -.swap-active:where(.swap-flip) .swap-on, -.swap-flip input:indeterminate ~ .swap-indeterminate { - transform: rotateY(0deg); -} - -.tabs-lifted > .tab:focus-visible { - border-end-end-radius: 0; - border-end-start-radius: 0; -} - -.tab:is(.tab-active, [aria-selected="true"]):not(.tab-disabled):not([disabled]), .tab:is(input:checked) { - border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity))); - --tw-border-opacity: 1; - --tw-text-opacity: 1; -} - -.tab:focus { - outline: 2px solid transparent; - outline-offset: 2px; -} - -.tab:focus-visible { - outline: 2px solid currentColor; - outline-offset: -5px; -} - -.tab-disabled, - .tab[disabled] { - cursor: not-allowed; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); - --tw-text-opacity: 0.2; -} - -.tabs-bordered > .tab { - border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity))); - --tw-border-opacity: 0.2; - border-style: solid; - border-bottom-width: calc(var(--tab-border, 1px) + 1px); -} - -.tabs-lifted > .tab { - border: var(--tab-border, 1px) solid transparent; - border-width: 0 0 var(--tab-border, 1px) 0; - border-start-start-radius: var(--tab-radius, 0.5rem); - border-start-end-radius: var(--tab-radius, 0.5rem); - border-bottom-color: var(--tab-border-color); - padding-inline-start: var(--tab-padding, 1rem); - padding-inline-end: var(--tab-padding, 1rem); - padding-top: var(--tab-border, 1px); -} - -.tabs-lifted > .tab:is(.tab-active, [aria-selected="true"]):not(.tab-disabled):not([disabled]), .tabs-lifted > .tab:is(input:checked) { - background-color: var(--tab-bg); - border-width: var(--tab-border, 1px) var(--tab-border, 1px) 0 var(--tab-border, 1px); - border-inline-start-color: var(--tab-border-color); - border-inline-end-color: var(--tab-border-color); - border-top-color: var(--tab-border-color); - padding-inline-start: calc(var(--tab-padding, 1rem) - var(--tab-border, 1px)); - padding-inline-end: calc(var(--tab-padding, 1rem) - var(--tab-border, 1px)); - padding-bottom: var(--tab-border, 1px); - padding-top: 0; -} - -.tabs-lifted > .tab:is(.tab-active, [aria-selected="true"]):not(.tab-disabled):not([disabled]):before, .tabs-lifted > .tab:is(input:checked):before { - z-index: 1; - content: ""; - display: block; - position: absolute; - width: calc(100% + var(--tab-radius, 0.5rem) * 2); - height: var(--tab-radius, 0.5rem); - bottom: 0; - background-size: var(--tab-radius, 0.5rem); - background-position: top left, - top right; - background-repeat: no-repeat; - --tab-grad: calc(69% - var(--tab-border, 1px)); - --radius-start: radial-gradient( - circle at top left, - transparent var(--tab-grad), - var(--tab-border-color) calc(var(--tab-grad) + 0.25px), - var(--tab-border-color) calc(var(--tab-grad) + var(--tab-border, 1px)), - var(--tab-bg) calc(var(--tab-grad) + var(--tab-border, 1px) + 0.25px) - ); - --radius-end: radial-gradient( - circle at top right, - transparent var(--tab-grad), - var(--tab-border-color) calc(var(--tab-grad) + 0.25px), - var(--tab-border-color) calc(var(--tab-grad) + var(--tab-border, 1px)), - var(--tab-bg) calc(var(--tab-grad) + var(--tab-border, 1px) + 0.25px) - ); - background-image: var(--radius-start), var(--radius-end); -} - -.tabs-lifted > .tab:is(.tab-active, [aria-selected="true"]):not(.tab-disabled):not([disabled]):first-child:before, .tabs-lifted > .tab:is(input:checked):first-child:before { - background-image: var(--radius-end); - background-position: top right; -} - -[dir="rtl"] .tabs-lifted > .tab:is(.tab-active, [aria-selected="true"]):not(.tab-disabled):not([disabled]):first-child:before, [dir="rtl"] .tabs-lifted > .tab:is(input:checked):first-child:before { - background-image: var(--radius-start); - background-position: top left; -} - -.tabs-lifted > .tab:is(.tab-active, [aria-selected="true"]):not(.tab-disabled):not([disabled]):last-child:before, .tabs-lifted > .tab:is(input:checked):last-child:before { - background-image: var(--radius-start); - background-position: top left; -} - -[dir="rtl"] .tabs-lifted > .tab:is(.tab-active, [aria-selected="true"]):not(.tab-disabled):not([disabled]):last-child:before, [dir="rtl"] .tabs-lifted > .tab:is(input:checked):last-child:before { - background-image: var(--radius-end); - background-position: top right; -} - -.tabs-lifted - > :is(.tab-active, [aria-selected="true"]):not(.tab-disabled):not([disabled]) - + .tabs-lifted - :is(.tab-active, [aria-selected="true"]):not(.tab-disabled):not([disabled]):before, .tabs-lifted > .tab:is(input:checked) + .tabs-lifted .tab:is(input:checked):before { - background-image: var(--radius-end); - background-position: top right; -} - -.tabs-boxed .tab { - border-radius: var(--rounded-btn, 0.5rem); -} - -.table:where([dir="rtl"], [dir="rtl"] *) { - text-align: right; -} - -.table :where(th, td) { - padding-left: 1rem; - padding-right: 1rem; - padding-top: 0.75rem; - padding-bottom: 0.75rem; - vertical-align: middle; -} - -.table tr.active, - .table tr.active:nth-child(even), - .table-zebra tbody tr:nth-child(even) { - --tw-bg-opacity: 1; - background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); -} - -.table-zebra tr.active, - .table-zebra tr.active:nth-child(even), - .table-zebra-zebra tbody tr:nth-child(even) { - --tw-bg-opacity: 1; - background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity))); -} - -.table :where(thead tr, tbody tr:not(:last-child), tbody tr:first-child:last-child) { - border-bottom-width: 1px; - --tw-border-opacity: 1; - border-bottom-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); -} - -.table :where(thead, tfoot) { - white-space: nowrap; - font-size: 0.75rem; - line-height: 1rem; - font-weight: 700; - color: var(--fallback-bc,oklch(var(--bc)/0.6)); -} - -.table :where(tfoot) { - border-top-width: 1px; - --tw-border-opacity: 1; - border-top-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); -} - -.textarea-bordered { - border-color: var(--fallback-bc,oklch(var(--bc)/0.2)); -} - -.textarea:focus { - box-shadow: none; - border-color: var(--fallback-bc,oklch(var(--bc)/0.2)); - outline-style: solid; - outline-width: 2px; - outline-offset: 2px; - outline-color: var(--fallback-bc,oklch(var(--bc)/0.2)); -} - -.textarea-disabled, - .textarea:disabled, - .textarea[disabled] { - cursor: not-allowed; - --tw-border-opacity: 1; - border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); - --tw-bg-opacity: 1; - background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); - color: var(--fallback-bc,oklch(var(--bc)/0.4)); -} - -.textarea-disabled::-moz-placeholder, .textarea:disabled::-moz-placeholder, .textarea[disabled]::-moz-placeholder { - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); - --tw-placeholder-opacity: 0.2; -} - -.textarea-disabled::placeholder, - .textarea:disabled::placeholder, - .textarea[disabled]::placeholder { - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); - --tw-placeholder-opacity: 0.2; -} - -@keyframes toast-pop { - 0% { - transform: scale(0.9); - opacity: 0; - } - - 100% { - transform: scale(1); - opacity: 1; - } -} - -[dir="rtl"] .toggle { - --handleoffsetcalculator: calc(var(--handleoffset) * 1); -} - -.toggle:focus-visible { - outline-style: solid; - outline-width: 2px; - outline-offset: 2px; - outline-color: var(--fallback-bc,oklch(var(--bc)/0.2)); -} - -.toggle:hover { - background-color: currentColor; -} - -.toggle:checked, - .toggle[aria-checked="true"] { - background-image: none; - --handleoffsetcalculator: var(--handleoffset); - --tw-text-opacity: 1; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); -} - -[dir="rtl"] .toggle:checked, [dir="rtl"] .toggle[aria-checked="true"] { - --handleoffsetcalculator: calc(var(--handleoffset) * -1); -} - -.toggle:indeterminate { - --tw-text-opacity: 1; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); - box-shadow: calc(var(--handleoffset) / 2) 0 0 2px var(--tglbg) inset, - calc(var(--handleoffset) / -2) 0 0 2px var(--tglbg) inset, - 0 0 0 2px var(--tglbg) inset; -} - -[dir="rtl"] .toggle:indeterminate { - box-shadow: calc(var(--handleoffset) / 2) 0 0 2px var(--tglbg) inset, - calc(var(--handleoffset) / -2) 0 0 2px var(--tglbg) inset, - 0 0 0 2px var(--tglbg) inset; -} - -.toggle-primary:focus-visible { - outline-color: var(--fallback-p,oklch(var(--p)/1)); -} - -.toggle-primary:checked, - .toggle-primary[aria-checked="true"] { - border-color: var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity))); - --tw-border-opacity: 0.1; - --tw-bg-opacity: 1; - background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); -} - -.toggle-success:focus-visible { - outline-color: var(--fallback-su,oklch(var(--su)/1)); -} - -.toggle-success:checked, - .toggle-success[aria-checked="true"] { - border-color: var(--fallback-su,oklch(var(--su)/var(--tw-border-opacity))); - --tw-border-opacity: 0.1; - --tw-bg-opacity: 1; - background-color: var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity))); -} - -.toggle:disabled { - cursor: not-allowed; - --tw-border-opacity: 1; - border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity))); - background-color: transparent; - opacity: 0.3; - --togglehandleborder: 0 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset, - var(--handleoffsetcalculator) 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset; -} - -.artboard.phone { - width: 320px; -} - -.badge-sm { - height: 1rem; - font-size: 0.75rem; - line-height: 1rem; - padding-left: 0.438rem; - padding-right: 0.438rem; -} - -.badge-lg { - height: 1.5rem; - font-size: 1rem; - line-height: 1.5rem; - padding-left: 0.688rem; - padding-right: 0.688rem; -} - -.btn-xs { - height: 1.5rem; - min-height: 1.5rem; - padding-left: 0.5rem; - padding-right: 0.5rem; - font-size: 0.75rem; -} - -.btn-sm { - height: 2rem; - min-height: 2rem; - padding-left: 0.75rem; - padding-right: 0.75rem; - font-size: 0.875rem; -} - -.btn-block { - width: 100%; -} - -.btn-square:where(.btn-xs) { - height: 1.5rem; - width: 1.5rem; - padding: 0px; -} - -.btn-square:where(.btn-sm) { - height: 2rem; - width: 2rem; - padding: 0px; -} - -.btn-square:where(.btn-md) { - height: 3rem; - width: 3rem; - padding: 0px; -} - -.btn-square:where(.btn-lg) { - height: 4rem; - width: 4rem; - padding: 0px; -} - -.btn-circle:where(.btn-xs) { - height: 1.5rem; - width: 1.5rem; - border-radius: 9999px; - padding: 0px; -} - -.btn-circle:where(.btn-sm) { - height: 2rem; - width: 2rem; - border-radius: 9999px; - padding: 0px; -} - -.btn-circle:where(.btn-md) { - height: 3rem; - width: 3rem; - border-radius: 9999px; - padding: 0px; -} - -.btn-circle:where(.btn-lg) { - height: 4rem; - width: 4rem; - border-radius: 9999px; - padding: 0px; -} - -[type="checkbox"].checkbox-xs { - height: 1rem; - width: 1rem; -} - -.divider-horizontal { - flex-direction: column; -} - -.divider-horizontal:before { - height: 100%; - width: 0.125rem; -} - -.divider-horizontal:after { - height: 100%; - width: 0.125rem; -} - -.drawer-open > .drawer-toggle { - display: none; -} - -.drawer-open > .drawer-toggle ~ .drawer-side { - pointer-events: auto; - visibility: visible; - position: sticky; - display: block; - width: auto; - overscroll-behavior: auto; -} - -.drawer-open > .drawer-toggle ~ .drawer-side > *:not(.drawer-overlay) { - transform: translateX(0%); -} - -[dir="rtl"] .drawer-open > .drawer-toggle ~ .drawer-side > *:not(.drawer-overlay) { - transform: translateX(0%); -} - -.drawer-open > .drawer-toggle:checked ~ .drawer-side { - pointer-events: auto; - visibility: visible; -} - -.drawer-open > .drawer-side { - overflow-y: auto; -} - -html:has(.drawer-toggle:checked) { - overflow-y: hidden; - scrollbar-gutter: stable; -} - -.indicator :where(.indicator-item) { - bottom: auto; - inset-inline-end: 0px; - inset-inline-start: auto; - top: 0px; - --tw-translate-y: -50%; - --tw-translate-x: 50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.indicator :where(.indicator-item):where([dir="rtl"], [dir="rtl"] *) { - --tw-translate-x: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.indicator :where(.indicator-item.indicator-start) { - inset-inline-end: auto; - inset-inline-start: 0px; - --tw-translate-x: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.indicator :where(.indicator-item.indicator-start):where([dir="rtl"], [dir="rtl"] *) { - --tw-translate-x: 50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.indicator :where(.indicator-item.indicator-center) { - inset-inline-end: 50%; - inset-inline-start: 50%; - --tw-translate-x: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.indicator :where(.indicator-item.indicator-center):where([dir="rtl"], [dir="rtl"] *) { - --tw-translate-x: 50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.indicator :where(.indicator-item.indicator-end) { - inset-inline-end: 0px; - inset-inline-start: auto; - --tw-translate-x: 50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.indicator :where(.indicator-item.indicator-end):where([dir="rtl"], [dir="rtl"] *) { - --tw-translate-x: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.indicator :where(.indicator-item.indicator-bottom) { - bottom: 0px; - top: auto; - --tw-translate-y: 50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.indicator :where(.indicator-item.indicator-middle) { - bottom: 50%; - top: 50%; - --tw-translate-y: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.indicator :where(.indicator-item.indicator-top) { - bottom: auto; - top: 0px; - --tw-translate-y: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.join.join-vertical { - flex-direction: column; -} - -.join.join-vertical .join-item:first-child:not(:last-child), - .join.join-vertical *:first-child:not(:last-child) .join-item { - border-end-start-radius: 0; - border-end-end-radius: 0; - border-start-start-radius: inherit; - border-start-end-radius: inherit; -} - -.join.join-vertical .join-item:last-child:not(:first-child), - .join.join-vertical *:last-child:not(:first-child) .join-item { - border-start-start-radius: 0; - border-start-end-radius: 0; - border-end-start-radius: inherit; - border-end-end-radius: inherit; -} - -.join.join-horizontal { - flex-direction: row; -} - -.join.join-horizontal .join-item:first-child:not(:last-child), - .join.join-horizontal *:first-child:not(:last-child) .join-item { - border-end-end-radius: 0; - border-start-end-radius: 0; - border-end-start-radius: inherit; - border-start-start-radius: inherit; -} - -.join.join-horizontal .join-item:last-child:not(:first-child), - .join.join-horizontal *:last-child:not(:first-child) .join-item { - border-end-start-radius: 0; - border-start-start-radius: 0; - border-end-end-radius: inherit; - border-start-end-radius: inherit; -} - -.menu-horizontal { - display: inline-flex; - flex-direction: row; -} - -.menu-horizontal > li:not(.menu-title) > details > ul { - position: absolute; -} - -.modal-bottom { - place-items: end; -} - -.steps-horizontal .step { - display: grid; - grid-template-columns: repeat(1, minmax(0, 1fr)); - grid-template-rows: repeat(2, minmax(0, 1fr)); - place-items: center; - text-align: center; -} - -.steps-vertical { - grid-auto-rows: 1fr; - grid-auto-flow: row; -} - -.steps-vertical .step { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - grid-template-rows: repeat(1, minmax(0, 1fr)); -} - -.tabs-md :where(.tab) { - height: 2rem; - font-size: 0.875rem; - line-height: 1.25rem; - line-height: 2; - --tab-padding: 1rem; -} - -.tabs-lg :where(.tab) { - height: 3rem; - font-size: 1.125rem; - line-height: 1.75rem; - line-height: 2; - --tab-padding: 1.25rem; -} - -.tabs-sm :where(.tab) { - height: 1.5rem; - font-size: 0.875rem; - line-height: .75rem; - --tab-padding: 0.75rem; -} - -.tabs-xs :where(.tab) { - height: 1.25rem; - font-size: 0.75rem; - line-height: .75rem; - --tab-padding: 0.5rem; -} - -[type="checkbox"].toggle-sm { - --handleoffset: 0.75rem; - height: 1.25rem; - width: 2rem; -} - -.avatar.online:before { - content: ""; - position: absolute; - z-index: 10; - display: block; - border-radius: 9999px; - --tw-bg-opacity: 1; - background-color: var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity))); - outline-style: solid; - outline-width: 2px; - outline-color: var(--fallback-b1,oklch(var(--b1)/1)); - width: 15%; - height: 15%; - top: 7%; - right: 7%; -} - -.avatar.offline:before { - content: ""; - position: absolute; - z-index: 10; - display: block; - border-radius: 9999px; - --tw-bg-opacity: 1; - background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity))); - outline-style: solid; - outline-width: 2px; - outline-color: var(--fallback-b1,oklch(var(--b1)/1)); - width: 15%; - height: 15%; - top: 7%; - right: 7%; -} - -.card-compact .card-body { - padding: 1rem; - font-size: 0.875rem; - line-height: 1.25rem; -} - -.card-compact .card-title { - margin-bottom: 0.25rem; -} - -.card-normal .card-body { - padding: var(--padding-card, 2rem); - font-size: 1rem; - line-height: 1.5rem; -} - -.card-normal .card-title { - margin-bottom: 0.75rem; -} - -.divider-horizontal { - margin-left: 1rem; - margin-right: 1rem; - margin-top: 0px; - margin-bottom: 0px; - height: auto; - width: 1rem; -} - -.drawer-open > .drawer-toggle ~ .drawer-side > .drawer-overlay { - cursor: default; - background-color: transparent; -} - -.join.join-vertical > :where(*:not(:first-child)) { - margin-left: 0px; - margin-right: 0px; - margin-top: -1px; -} - -.join.join-vertical > :where(*:not(:first-child)):is(.btn) { - margin-top: calc(var(--border-btn) * -1); -} - -.join.join-horizontal > :where(*:not(:first-child)) { - margin-top: 0px; - margin-bottom: 0px; - margin-inline-start: -1px; -} - -.join.join-horizontal > :where(*:not(:first-child)):is(.btn) { - margin-inline-start: calc(var(--border-btn) * -1); -} - -.menu-horizontal > li:not(.menu-title) > details > ul { - margin-inline-start: 0px; - margin-top: 1rem; - padding-top: 0.5rem; - padding-bottom: 0.5rem; - padding-inline-end: 0.5rem; -} - -.menu-horizontal > li > details > ul:before { - content: none; -} - -:where(.menu-horizontal > li:not(.menu-title) > details > ul) { - border-radius: var(--rounded-box, 1rem); - --tw-bg-opacity: 1; - background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); - --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); - --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.menu-sm :where(li:not(.menu-title) > *:not(ul, details, .menu-title)), .menu-sm :where(li:not(.menu-title) > details > summary:not(.menu-title)) { - border-radius: var(--rounded-btn, 0.5rem); - padding-left: 0.75rem; - padding-right: 0.75rem; - padding-top: 0.25rem; - padding-bottom: 0.25rem; - font-size: 0.875rem; - line-height: 1.25rem; -} - -.menu-sm .menu-title { - padding-left: 0.75rem; - padding-right: 0.75rem; - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} - -.modal-top :where(.modal-box) { - width: 100%; - max-width: none; - --tw-translate-y: -2.5rem; - --tw-scale-x: 1; - --tw-scale-y: 1; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - border-bottom-right-radius: var(--rounded-box, 1rem); - border-bottom-left-radius: var(--rounded-box, 1rem); - border-top-left-radius: 0px; - border-top-right-radius: 0px; -} - -.modal-middle :where(.modal-box) { - width: 91.666667%; - max-width: 32rem; - --tw-translate-y: 0px; - --tw-scale-x: .9; - --tw-scale-y: .9; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - border-top-left-radius: var(--rounded-box, 1rem); - border-top-right-radius: var(--rounded-box, 1rem); - border-bottom-right-radius: var(--rounded-box, 1rem); - border-bottom-left-radius: var(--rounded-box, 1rem); -} - -.modal-bottom :where(.modal-box) { - width: 100%; - max-width: none; - --tw-translate-y: 2.5rem; - --tw-scale-x: 1; - --tw-scale-y: 1; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - border-top-left-radius: var(--rounded-box, 1rem); - border-top-right-radius: var(--rounded-box, 1rem); - border-bottom-right-radius: 0px; - border-bottom-left-radius: 0px; -} - -.steps-horizontal .step { - grid-template-rows: 40px 1fr; - grid-template-columns: auto; - min-width: 4rem; -} - -.steps-horizontal .step:before { - height: 0.5rem; - width: 100%; - --tw-translate-x: 0px; - --tw-translate-y: 0px; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - content: ""; - margin-inline-start: -100%; -} - -.steps-horizontal .step:where([dir="rtl"], [dir="rtl"] *):before { - --tw-translate-x: 0px; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.steps-vertical .step { - gap: 0.5rem; - grid-template-columns: 40px 1fr; - grid-template-rows: auto; - min-height: 4rem; - justify-items: start; -} - -.steps-vertical .step:before { - height: 100%; - width: 0.5rem; - --tw-translate-x: -50%; - --tw-translate-y: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - margin-inline-start: 50%; -} - -.steps-vertical .step:where([dir="rtl"], [dir="rtl"] *):before { - --tw-translate-x: 50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.alert { - grid-auto-flow: column; -} - -.container { - max-width: 100%; - width: 100%; -} - -@media (min-width: 640px) { - .container { - width: 100%; - } -} - -@media (min-width: 768px) { - .container { - width: 100%; - } -} - -@media (min-width: 1024px) { - .container { - width: 960px; - } -} - -.main-bg { - width: 100vw; - position: fixed; - left: 0; - top: 40vw; - opacity: 0.25; - z-index: -1; -} - -@media (min-width: 640px) { - .main-bg { - top: 50vw; - } -} - -@media (min-width: 768px) { - .main-bg { - top: 45vw; - } -} - -@media (min-width: 1024px) { - .main-bg { - top: 20vw; - } -} - -@media (min-width: 1280px) { - .main-bg { - top: 20vw; - } -} - -.main-bg.top { - top: 0; -} - -.header-hero { - max-width: 70vw; - margin: 0 auto; -} - -@media (min-width: 640px) { - .header-hero { - max-width: 90vw; - } -} - -@media (min-width: 768px) { - .header-hero { - max-width: 90vw; - } -} - -@media (min-width: 1024px) { - .header-hero { - max-width: 70vw; - } -} - -@media (min-width: 1280px) { - .header-hero { - max-width: 70vw; - } -} - -.tab:is(input[type="radio"]) { - border-bottom-right-radius: inherit; - border-bottom-left-radius: inherit; -} - -.carousel-control-left { - --btn-focus-scale: 1; - --animation-btn: 0; - --animation-input: 0; - position: absolute; - left: 0px; - top: 50%; - margin: -0.25rem; - margin-right: 2rem; - --tw-translate-y: -50%; - --tw-translate-x: -100%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.carousel-control-right { - --btn-focus-scale: 1; - --animation-btn: 0; - --animation-input: 0; - position: absolute; - right: 0px; - top: 50%; - margin: -0.25rem; - margin-left: 2rem; - --tw-translate-y: -50%; - --tw-translate-x: 100%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border-width: 0; -} - -.visible { - visibility: visible; -} - -.collapse { - visibility: collapse; -} - -.fixed { - position: fixed; -} - -.absolute { - position: absolute; -} - -.relative { - position: relative; -} - -.sticky { - position: sticky; -} - -.bottom-0 { - bottom: 0px; -} - -.left-0 { - left: 0px; -} - -.left-4 { - left: 1rem; -} - -.top-0 { - top: 0px; -} - -.top-1\/2 { - top: 50%; -} - -.top-12 { - top: 3rem; -} - -.z-10 { - z-index: 10; -} - -.z-40 { - z-index: 40; -} - -.z-50 { - z-index: 50; -} - -.z-\[10\] { - z-index: 10; -} - -.z-\[1\] { - z-index: 1; -} - -.z-\[9999\] { - z-index: 9999; -} - -.z-\[9\] { - z-index: 9; -} - -.col-span-2 { - grid-column: span 2 / span 2; -} - -.col-span-5 { - grid-column: span 5 / span 5; -} - -.float-end { - float: inline-end; -} - -.m-1 { - margin: 0.25rem; -} - -.mx-4 { - margin-left: 1rem; - margin-right: 1rem; -} - -.mx-auto { - margin-left: auto; - margin-right: auto; -} - -.my-1 { - margin-top: 0.25rem; - margin-bottom: 0.25rem; -} - -.my-2 { - margin-top: 0.5rem; - margin-bottom: 0.5rem; -} - -.my-3 { - margin-top: 0.75rem; - margin-bottom: 0.75rem; -} - -.my-4 { - margin-top: 1rem; - margin-bottom: 1rem; -} - -.my-8 { - margin-top: 2rem; - margin-bottom: 2rem; -} - -.-mb-1 { - margin-bottom: -0.25rem; -} - -.mb-11 { - margin-bottom: 2.75rem; -} - -.mb-2 { - margin-bottom: 0.5rem; -} - -.mb-4 { - margin-bottom: 1rem; -} - -.mb-5 { - margin-bottom: 1.25rem; -} - -.mb-8 { - margin-bottom: 2rem; -} - -.ml-4 { - margin-left: 1rem; -} - -.mr-2 { - margin-right: 0.5rem; -} - -.mr-4 { - margin-right: 1rem; -} - -.mr-5 { - margin-right: 1.25rem; -} - -.mt-16 { - margin-top: 4rem; -} - -.mt-2 { - margin-top: 0.5rem; -} - -.mt-20 { - margin-top: 5rem; -} - -.mt-3 { - margin-top: 0.75rem; -} - -.mt-4 { - margin-top: 1rem; -} - -.mt-5 { - margin-top: 1.25rem; -} - -.mt-8 { - margin-top: 2rem; -} - -.block { - display: block; -} - -.inline-block { - display: inline-block; -} - -.inline { - display: inline; -} - -.flex { - display: flex; -} - -.inline-flex { - display: inline-flex; -} - -.table { - display: table; -} - -.grid { - display: grid; -} - -.contents { - display: contents; -} - -.hidden { - display: none; -} - -.aspect-\[4\/3\] { - aspect-ratio: 4/3; -} - -.aspect-video { - aspect-ratio: 16 / 9; -} - -.size-4 { - width: 1rem; - height: 1rem; -} - -.size-5 { - width: 1.25rem; - height: 1.25rem; -} - -.h-0 { - height: 0px; -} - -.h-10 { - height: 2.5rem; -} - -.h-11 { - height: 2.75rem; -} - -.h-24 { - height: 6rem; -} - -.h-3 { - height: 0.75rem; -} - -.h-4 { - height: 1rem; -} - -.h-6 { - height: 1.5rem; -} - -.h-8 { - height: 2rem; -} - -.h-96 { - height: 24rem; -} - -.h-\[3px\] { - height: 3px; -} - -.h-\[calc\(100vh\)\] { - height: calc(100vh); -} - -.h-auto { - height: auto; -} - -.h-screen { - height: 100vh; -} - -.max-h-44 { - max-height: 11rem; -} - -.min-h-fit { - min-height: -moz-fit-content; - min-height: fit-content; -} - -.min-h-full { - min-height: 100%; -} - -.min-h-screen { - min-height: 100vh; -} - -.w-0 { - width: 0px; -} - -.w-1\/2 { - width: 50%; -} - -.w-10 { - width: 2.5rem; -} - -.w-10\/12 { - width: 83.333333%; -} - -.w-24 { - width: 6rem; -} - -.w-3 { - width: 0.75rem; -} - -.w-48 { - width: 12rem; -} - -.w-52 { - width: 13rem; -} - -.w-56 { - width: 14rem; -} - -.w-6 { - width: 1.5rem; -} - -.w-8 { - width: 2rem; -} - -.w-fit { - width: -moz-fit-content; - width: fit-content; -} - -.w-full { - width: 100%; -} - -.max-w-\[8rem\] { - max-width: 8rem; -} - -.max-w-full { - max-width: 100%; -} - -.max-w-prose { - max-width: 65ch; -} - -.max-w-xs { - max-width: 20rem; -} - -.flex-1 { - flex: 1 1 0%; -} - -.flex-auto { - flex: 1 1 auto; -} - -.flex-none { - flex: none; -} - -.flex-shrink-0 { - flex-shrink: 0; -} - -.shrink-0 { - flex-shrink: 0; -} - -.flex-grow { - flex-grow: 1; -} - -.grow { - flex-grow: 1; -} - -.grow-0 { - flex-grow: 0; -} - -.-translate-y-1\/2 { - --tw-translate-y: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.cursor-pointer { - cursor: pointer; -} - -.resize-none { - resize: none; -} - -.snap-start { - scroll-snap-align: start; -} - -.list-inside { - list-style-position: inside; -} - -.list-disc { - list-style-type: disc; -} - -.grid-flow-col { - grid-auto-flow: column; -} - -.grid-cols-1 { - grid-template-columns: repeat(1, minmax(0, 1fr)); -} - -.grid-cols-7 { - grid-template-columns: repeat(7, minmax(0, 1fr)); -} - -.flex-col { - flex-direction: column; -} - -.flex-wrap { - flex-wrap: wrap; -} - -.place-items-center { - place-items: center; -} - -.items-start { - align-items: flex-start; -} - -.items-center { - align-items: center; -} - -.justify-normal { - justify-content: normal; -} - -.justify-end { - justify-content: flex-end; -} - -.justify-center { - justify-content: center; -} - -.justify-between { - justify-content: space-between; -} - -.justify-stretch { - justify-content: stretch; -} - -.gap-1 { - gap: 0.25rem; -} - -.gap-2 { - gap: 0.5rem; -} - -.gap-3 { - gap: 0.75rem; -} - -.gap-4 { - gap: 1rem; -} - -.gap-6 { - gap: 1.5rem; -} - -.space-x-2 > :not([hidden]) ~ :not([hidden]) { - --tw-space-x-reverse: 0; - margin-right: calc(0.5rem * var(--tw-space-x-reverse)); - margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); -} - -.space-x-4 > :not([hidden]) ~ :not([hidden]) { - --tw-space-x-reverse: 0; - margin-right: calc(1rem * var(--tw-space-x-reverse)); - margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); -} - -.space-y-4 > :not([hidden]) ~ :not([hidden]) { - --tw-space-y-reverse: 0; - margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); - margin-bottom: calc(1rem * var(--tw-space-y-reverse)); -} - -.self-start { - align-self: flex-start; -} - -.overflow-hidden { - overflow: hidden; -} - -.overflow-scroll { - overflow: scroll; -} - -.overflow-y-auto { - overflow-y: auto; -} - -.whitespace-nowrap { - white-space: nowrap; -} - -.whitespace-pre-wrap { - white-space: pre-wrap; -} - -.rounded-box { - border-radius: var(--rounded-box, 1rem); -} - -.rounded-full { - border-radius: 9999px; -} - -.rounded-lg { - border-radius: 0.5rem; -} - -.rounded-md { - border-radius: 0.375rem; -} - -.rounded-e-lg { - border-start-end-radius: 0.5rem; - border-end-end-radius: 0.5rem; -} - -.rounded-s-lg { - border-start-start-radius: 0.5rem; - border-end-start-radius: 0.5rem; -} - -.border { - border-width: 1px; -} - -.border-2 { - border-width: 2px; -} - -.border-x-0 { - border-left-width: 0px; - border-right-width: 0px; -} - -.border-b { - border-bottom-width: 1px; -} - -.border-b-2 { - border-bottom-width: 2px; -} - -.border-t-2 { - border-top-width: 2px; -} - -.border-base-200 { - --tw-border-opacity: 1; - border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); -} - -.border-base-300 { - --tw-border-opacity: 1; - border-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity))); -} - -.border-gray-300 { - --tw-border-opacity: 1; - border-color: rgb(209 213 219 / var(--tw-border-opacity)); -} - -.border-gray-600 { - --tw-border-opacity: 1; - border-color: rgb(75 85 99 / var(--tw-border-opacity)); -} - -.border-gray-700 { - --tw-border-opacity: 1; - border-color: rgb(55 65 81 / var(--tw-border-opacity)); -} - -.border-red-500 { - --tw-border-opacity: 1; - border-color: rgb(239 68 68 / var(--tw-border-opacity)); -} - -.border-slate-300 { - --tw-border-opacity: 1; - border-color: rgb(203 213 225 / var(--tw-border-opacity)); -} - -.border-white { - --tw-border-opacity: 1; - border-color: rgb(255 255 255 / var(--tw-border-opacity)); -} - -.border-opacity-100 { - --tw-border-opacity: 1; -} - -.border-opacity-80 { - --tw-border-opacity: 0.8; -} - -.bg-\[\#00ff00\] { - --tw-bg-opacity: 1; - background-color: rgb(0 255 0 / var(--tw-bg-opacity)); -} - -.bg-base-100 { - --tw-bg-opacity: 1; - background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); -} - -.bg-base-200 { - --tw-bg-opacity: 1; - background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); -} - -.bg-black { - --tw-bg-opacity: 1; - background-color: rgb(0 0 0 / var(--tw-bg-opacity)); -} - -.bg-gray-200 { - --tw-bg-opacity: 1; - background-color: rgb(229 231 235 / var(--tw-bg-opacity)); -} - -.bg-gray-50 { - --tw-bg-opacity: 1; - background-color: rgb(249 250 251 / var(--tw-bg-opacity)); -} - -.bg-gray-700 { - --tw-bg-opacity: 1; - background-color: rgb(55 65 81 / var(--tw-bg-opacity)); -} - -.bg-primary { - --tw-bg-opacity: 1; - background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); -} - -.bg-slate-100 { - --tw-bg-opacity: 1; - background-color: rgb(241 245 249 / var(--tw-bg-opacity)); -} - -.bg-success { - --tw-bg-opacity: 1; - background-color: var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity))); -} - -.bg-opacity-75 { - --tw-bg-opacity: 0.75; -} - -.bg-opacity-90 { - --tw-bg-opacity: 0.9; -} - -.bg-gradient-to-t { - background-image: linear-gradient(to top, var(--tw-gradient-stops)); -} - -.from-black\/70 { - --tw-gradient-from: rgb(0 0 0 / 0.7) var(--tw-gradient-from-position); - --tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.from-80\% { - --tw-gradient-from-position: 80%; -} - -.to-transparent { - --tw-gradient-to: transparent var(--tw-gradient-to-position); -} - -.fill-current { - fill: currentColor; -} - -.stroke-current { - stroke: currentColor; -} - -.object-cover { - -o-object-fit: cover; - object-fit: cover; -} - -.p-10 { - padding: 2.5rem; -} - -.p-2 { - padding: 0.5rem; -} - -.p-3 { - padding: 0.75rem; -} - -.p-4 { - padding: 1rem; -} - -.px-1 { - padding-left: 0.25rem; - padding-right: 0.25rem; -} - -.px-3 { - padding-left: 0.75rem; - padding-right: 0.75rem; -} - -.px-4 { - padding-left: 1rem; - padding-right: 1rem; -} - -.px-5 { - padding-left: 1.25rem; - padding-right: 1.25rem; -} - -.py-2 { - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} - -.py-2\.5 { - padding-top: 0.625rem; - padding-bottom: 0.625rem; -} - -.py-3 { - padding-top: 0.75rem; - padding-bottom: 0.75rem; -} - -.py-4 { - padding-top: 1rem; - padding-bottom: 1rem; -} - -.pb-24 { - padding-bottom: 6rem; -} - -.pb-3 { - padding-bottom: 0.75rem; -} - -.pb-5 { - padding-bottom: 1.25rem; -} - -.pb-6 { - padding-bottom: 1.5rem; -} - -.pl-11 { - padding-left: 2.75rem; -} - -.pl-2 { - padding-left: 0.5rem; -} - -.pl-3 { - padding-left: 0.75rem; -} - -.pl-4 { - padding-left: 1rem; -} - -.pr-2 { - padding-right: 0.5rem; -} - -.pr-4 { - padding-right: 1rem; -} - -.text-center { - text-align: center; -} - -.text-2xl { - font-size: 1.563rem; -} - -.text-3xl { - font-size: 1.953rem; -} - -.text-lg { - font-size: 1.15rem; -} - -.text-sm { - font-size: 0.8rem; -} - -.text-xl { - font-size: 1.25rem; -} - -.font-bold { - font-weight: 700; -} - -.font-medium { - font-weight: 500; -} - -.font-normal { - font-weight: 400; -} - -.font-semibold { - font-weight: 600; -} - -.tracking-wide { - letter-spacing: 0.025em; -} - -.text-black { - --tw-text-opacity: 1; - color: rgb(0 0 0 / var(--tw-text-opacity)); -} - -.text-gray-900 { - --tw-text-opacity: 1; - color: rgb(17 24 39 / var(--tw-text-opacity)); -} - -.text-neutral-content { - --tw-text-opacity: 1; - color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); -} - -.text-primary { - --tw-text-opacity: 1; - color: var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity))); -} - -.text-primary-content { - --tw-text-opacity: 1; - color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); -} - -.text-red-500 { - --tw-text-opacity: 1; - color: rgb(239 68 68 / var(--tw-text-opacity)); -} - -.text-slate-700 { - --tw-text-opacity: 1; - color: rgb(51 65 85 / var(--tw-text-opacity)); -} - -.text-slate-700\/50 { - color: rgb(51 65 85 / 0.5); -} - -.text-white { - --tw-text-opacity: 1; - color: rgb(255 255 255 / var(--tw-text-opacity)); -} - -.placeholder-gray-400::-moz-placeholder { - --tw-placeholder-opacity: 1; - color: rgb(156 163 175 / var(--tw-placeholder-opacity)); -} - -.placeholder-gray-400::placeholder { - --tw-placeholder-opacity: 1; - color: rgb(156 163 175 / var(--tw-placeholder-opacity)); -} - -.opacity-0 { - opacity: 0; -} - -.opacity-100 { - opacity: 1; -} - -.opacity-50 { - opacity: 0.5; -} - -.shadow { - --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.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); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.shadow-md { - --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.filter { - filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); -} - -.transition { - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; -} - -.transition-\[width\] { - transition-property: width; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; -} - -.transition-all { - transition-property: all; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; -} - -.duration-300 { - transition-duration: 300ms; -} - -.ease-out { - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); -} - -@tailwind typography; - -@tailwind layout; - -body { - padding-top: 5rem; -} - -body:has(.bottom-drawer) { - padding-bottom: 10rem; -} - -.sticky-under-top-nav { - top: 4rem; -} - -.sticky-under-top-nav.subheader { - top: 8rem; -} - -.sticky-under-top-nav + *:first-child { - margin-top: 4rem; -} - -.brand-logo { - width: 3rem; - height: 4rem; -} - -.brand-type { - width: 7.7rem; -} - -.drawer-end .drawer-toggle ~ .drawer-side { - z-index: 999; -} - -.menu .collapse .collapse-title { - padding: 0; - min-height: 1.75rem; -} - -.menu .collapse-content { - padding-left: 0; - padding-right: 0; - overflow: initial; -} - -.menu :where(.menu li) { - flex-wrap: nowrap; -} - -.menu li > *:not(ul, .menu-title, details, .btn):active, -.menu li > *:not(ul, .menu-title, details, .btn).active, -.menu li > details > summary:active { - background-color: transparent; -} - -.menu :where(li:not(.menu-title) > *:not(ul, details, .menu-title, .btn)), -.menu :where(li:not(.menu-title) > details > summary:not(.menu-title)) { - gap: 0; - grid-auto-columns: initial; - padding-left: 0.5rem; - padding-right: 0.5rem; -} - -.dropdown-content { - width: -moz-max-content; - width: max-content; -} - -.dropdown-content.menu li { - flex-direction: row; -} - -.dropdown-content.menu .label { - justify-content: flex-start; -} - -.dropdown-content .collapse-title, -:where(.dropdown-content .collapse > input[type='checkbox']), -:where(.dropdown-content .collapse > input[type='radio']) { - /* min-height: 0; */ - min-height: 1.5rem; -} - -.htmx-indicator { - display: none; -} - -.htmx-request .htmx-indicator { - display: inherit; -} - -.htmx-request .htmx-indicator.flex { - display: flex; -} - -.htmx-show-in-flight { - display: none; -} - -.htmx-request .htmx-show-in-flight { - display: inherit; -} - -.htmx-hide-in-flight { - display: inherit; -} - -.htmx-request .htmx-hide-in-flight { - display: none; -} - -.margins-when-children.my-8:has(*) { - margin: 2rem 0; -} - -/* BEGIN seshu ingestion "add event source" section */ - -#event-source-steps .step { - min-width: 15rem; -} - -.checkbox-card { - border: 2px solid oklch(var(--er)); -} - -.checkbox-card .checkbox-card-header { - border-top-left-radius: var(--rounded-btn); - border-top-right-radius: var(--rounded-btn); -} - -.checkbox-card .checkbox-card-header .label { - justify-content: center; -} - -.checkbox-card .checkbox-card-header:has(input[type='checkbox']) { - background-color: oklch(var(--er)); - color: oklch(var(--erc)); -} - -.checkbox-card .checkbox-card-header:has(input[type='checkbox']:checked) { - background-color: oklch(var(--su)); - color: oklch(var(--suc)); -} - -.checkbox-card:has(input[id*='main-toggle-'][type='checkbox']:checked) { - border-color: oklch(var(--su)); -} - -.has-toggleable-text:has(input[type='checkbox']) .hidden-when-checked { - display: inherit; -} - -.has-toggleable-text:has(input[type='checkbox']:checked) .hidden-when-checked { - display: none; -} - -.has-toggleable-text:not(:has(input[type='checkbox']:checked)) - .hidden-when-not-checked { - display: none; -} - -#event-source-container:not(:has(#event-candidates-inner .checkbox-card)) - .candidates-loaded-visible, -#event-source-container:has(#event-candidates-inner .checkbox-card) - .candidates-loading-visible { - height: 0; - width: 0; - opacity: 0; - display: none; -} - -#event-source-container:has(#event-candidates-inner .checkbox-card) - #explainer-section - .alert-info { - background-color: oklch(var(--su)); -} - -/* END seshu ingestion "add event source" section */ - -.bottom-drawer { - position: fixed; - bottom: 0; - left: 0; - right: 0; - width: 100%; - z-index: 999; - height: auto; - /* TODO: FIX THIS! use tailwind variables */ - background: white; - padding: 20px; -} - -.btn.carousel-control-left:active:hover, .btn.carousel-control-left:active:focus { - transform: translate(-100%, -50%) -} - -.btn.carousel-control-right:active:hover, .btn.carousel-control-right:active:focus { - transform: translate(100%, -50%) -} - -.btn.btn-bold-outline { - border: 5px solid oklch(var(--p)); -} - -.btn-outline.btn-primary.text-neutral-content { - color: var(--fallback-nc, oklch(var(--nc) / var(--tw-text-opacity))); -} - -.drawer .collapse-content :where(.menu li) { - flex-direction: inherit; - align-items: center; -} - -/* Chrome, Safari, Edge, Opera */ - -input::-webkit-outer-spin-button, -input::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; -} - -/* Firefox */ - -input[type='number'] { - -moz-appearance: textfield; -} - -.tab:is(input[type='radio']) { - width: -moz-max-content; - width: max-content; -} - -.header-hero .opener { - font-size: 125%; -} - -.icon-container { - display: inline-flex; - justify-content: center; - vertical-align: middle; -} - -.page-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1.5rem; -} - -.table.top-align :where(th, td) { - vertical-align: top; -} - -@media (min-width: 640px) { - .sm\:modal-middle { - place-items: center; - } - - .sm\:modal-middle :where(.modal-box) { - width: 91.666667%; - max-width: 32rem; - --tw-translate-y: 0px; - --tw-scale-x: .9; - --tw-scale-y: .9; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - border-top-left-radius: var(--rounded-box, 1rem); - border-top-right-radius: var(--rounded-box, 1rem); - border-bottom-right-radius: var(--rounded-box, 1rem); - border-bottom-left-radius: var(--rounded-box, 1rem); - } -} - -.last\:mr-0:last-child { - margin-right: 0px; -} - -.hover\:bg-gray-500:hover { - --tw-bg-opacity: 1; - background-color: rgb(107 114 128 / var(--tw-bg-opacity)); -} - -.hover\:bg-gray-600:hover { - --tw-bg-opacity: 1; - background-color: rgb(75 85 99 / var(--tw-bg-opacity)); -} - -.hover\:bg-slate-800\/5:hover { - background-color: rgb(30 41 59 / 0.05); -} - -.hover\:bg-opacity-80:hover { - --tw-bg-opacity: 0.8; -} - -.hover\:text-black:hover { - --tw-text-opacity: 1; - color: rgb(0 0 0 / var(--tw-text-opacity)); -} - -.hover\:opacity-75:hover { - opacity: 0.75; -} - -.focus\:outline-none:focus { - outline: 2px solid transparent; - outline-offset: 2px; -} - -.focus\:ring-2:focus { - --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); - --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); - box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); -} - -.focus\:ring-gray-700:focus { - --tw-ring-opacity: 1; - --tw-ring-color: rgb(55 65 81 / var(--tw-ring-opacity)); -} - -.focus-visible\:border-green-700:focus-visible { - --tw-border-opacity: 1; - border-color: rgb(21 128 61 / var(--tw-border-opacity)); -} - -.focus-visible\:bg-slate-800\/5:focus-visible { - background-color: rgb(30 41 59 / 0.05); -} - -.focus-visible\:text-black:focus-visible { - --tw-text-opacity: 1; - color: rgb(0 0 0 / var(--tw-text-opacity)); -} - -.focus-visible\:outline-none:focus-visible { - outline: 2px solid transparent; - outline-offset: 2px; -} - -.focus-visible\:outline:focus-visible { - outline-style: solid; -} - -.focus-visible\:outline-2:focus-visible { - outline-width: 2px; -} - -.focus-visible\:outline-offset-2:focus-visible { - outline-offset: 2px; -} - -.focus-visible\:outline-green-700:focus-visible { - outline-color: #15803d; -} - -.disabled\:cursor-not-allowed:disabled { - cursor: not-allowed; -} - -.disabled\:opacity-75:disabled { - opacity: 0.75; -} - -.peer:checked ~ .peer-checked\:bg-base-200 { - --tw-bg-opacity: 1; - background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); -} - -@media (min-width: 768px) { - .md\:float-start { - float: inline-start; - } - - .md\:mb-3 { - margin-bottom: 0.75rem; - } - - .md\:mr-5 { - margin-right: 1.25rem; - } - - .md\:grid { - display: grid; - } - - .md\:aspect-\[16\/9\] { - aspect-ratio: 16/9; - } - - .md\:w-1\/2 { - width: 50%; - } - - .md\:grid-cols-2 { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .md\:grid-cols-7 { - grid-template-columns: repeat(7, minmax(0, 1fr)); - } - - .md\:place-items-center { - place-items: center; - } - - .md\:p-12 { - padding: 3rem; - } - - .md\:pb-4 { - padding-bottom: 1rem; - } - - .md\:pr-3 { - padding-right: 0.75rem; - } - - .md\:text-3xl { - font-size: 1.953rem; - } - - .md\:text-4xl { - font-size: 2.441rem; - } - - .md\:text-base { - font-size: 1rem; - } - - .md\:text-lg { - font-size: 1.15rem; - } - - .md\:text-xl { - font-size: 1.25rem; - } - - .md\:opacity-30 { - opacity: 0.3; - } - - .md\:hover\:opacity-75:hover { - opacity: 0.75; - } -} - -@media (min-width: 1024px) { - .lg\:col-span-2 { - grid-column: span 2 / span 2; - } - - .lg\:col-span-5 { - grid-column: span 5 / span 5; - } - - .lg\:inline-block { - display: inline-block; - } - - .lg\:flex { - display: flex; - } - - .lg\:inline-flex { - display: inline-flex; - } - - .lg\:hidden { - display: none; - } - - .lg\:w-1\/3 { - width: 33.333333%; - } - - .lg\:grid-cols-3 { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } - - .lg\:grid-cols-7 { - grid-template-columns: repeat(7, minmax(0, 1fr)); - } -} - -@media (prefers-color-scheme: dark) { - .dark\:border-slate-700 { - --tw-border-opacity: 1; - border-color: rgb(51 65 85 / var(--tw-border-opacity)); - } - - .dark\:bg-slate-800 { - --tw-bg-opacity: 1; - background-color: rgb(30 41 59 / var(--tw-bg-opacity)); - } - - .dark\:bg-slate-800\/50 { - background-color: rgb(30 41 59 / 0.5); - } - - .dark\:text-slate-300 { - --tw-text-opacity: 1; - color: rgb(203 213 225 / var(--tw-text-opacity)); - } - - .dark\:text-slate-300\/50 { - color: rgb(203 213 225 / 0.5); - } - - .dark\:hover\:bg-slate-100\/5:hover { - background-color: rgb(241 245 249 / 0.05); - } - - .dark\:hover\:text-white:hover { - --tw-text-opacity: 1; - color: rgb(255 255 255 / var(--tw-text-opacity)); - } - - .dark\:focus-visible\:border-green-600:focus-visible { - --tw-border-opacity: 1; - border-color: rgb(22 163 74 / var(--tw-border-opacity)); - } - - .dark\:focus-visible\:bg-slate-100\/10:focus-visible { - background-color: rgb(241 245 249 / 0.1); - } - - .dark\:focus-visible\:text-white:focus-visible { - --tw-text-opacity: 1; - color: rgb(255 255 255 / var(--tw-text-opacity)); - } - - .dark\:focus-visible\:outline-green-600:focus-visible { - outline-color: #16a34a; - } -}