The cards drag too, held by their headings

Same arrangement the provider panel gives its readings, applied to the four
charging cards: drag one and the column reorders live under the pointer, the
card being dragged goes half-transparent, the one it is over takes a ring, and
the rail's lock holds the lot still.

Two things differ from the readings row, both because these are four different
things rather than four of one. A card is placed with the CSS order property
instead of by moving markup, so each keeps its own template and its own v-if.
And the handle is the card's heading rather than the whole card — a card that
was draggable everywhere would fight the current-limit slider and the address
fields for the pointer.

Saved on the profile as charger_card_order, beside the tab order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tajniak81
2026-09-01 21:24:04 +02:00
co-authored by Claude Opus 5
parent 1418a566fd
commit 3c64d6e84c
8 changed files with 191 additions and 20 deletions
+31 -14
View File
@@ -43,7 +43,8 @@ type userRecord struct {
CarOrder json.RawMessage `json:"car_order"`
// The charging page's tab arrangement, stored the same way and for the same
// reason.
ChargerTabOrder json.RawMessage `json:"charger_tab_order"`
ChargerTabOrder json.RawMessage `json:"charger_tab_order"`
ChargerCardOrder json.RawMessage `json:"charger_card_order"`
}
// carOrder decodes the stored garage arrangement, treating anything unexpected
@@ -52,6 +53,8 @@ func (rec userRecord) carOrder() []string { return decodeStringList(rec.CarOrder
func (rec userRecord) chargerTabOrder() []string { return decodeStringList(rec.ChargerTabOrder) }
func (rec userRecord) chargerCardOrder() []string { return decodeStringList(rec.ChargerCardOrder) }
// decodeStringList reads a PocketBase json field that holds a list of strings,
// treating anything unexpected as empty rather than failing the whole read.
func decodeStringList(raw json.RawMessage) []string {
@@ -82,9 +85,10 @@ func (rec userRecord) toModel() models.User {
Role: orDefault(rec.Role, "user"),
Created: rec.Created,
Organization: rec.Organization,
CarOrder: rec.carOrder(),
ChargerTabOrder: rec.chargerTabOrder(),
Organization: rec.Organization,
CarOrder: rec.carOrder(),
ChargerTabOrder: rec.chargerTabOrder(),
ChargerCardOrder: rec.chargerCardOrder(),
}
if t := parsePBDate(rec.DeletionRequestedAt); !t.IsZero() {
u.DeletionRequestedAt = &t
@@ -133,16 +137,17 @@ func (s *Server) handleGetMe(w http.ResponseWriter, r *http.Request) {
}
type updateMeRequest struct {
Name *string `json:"name"`
Bio *string `json:"bio"`
Theme *string `json:"theme"`
Locale *string `json:"locale"`
DateFormat *string `json:"dateFormat"`
Currency *string `json:"currency"`
FontSize *string `json:"fontSize"`
DragLocked *bool `json:"dragLocked"`
CarOrder *[]string `json:"carOrder"`
ChargerTabOrder *[]string `json:"chargerTabOrder"`
Name *string `json:"name"`
Bio *string `json:"bio"`
Theme *string `json:"theme"`
Locale *string `json:"locale"`
DateFormat *string `json:"dateFormat"`
Currency *string `json:"currency"`
FontSize *string `json:"fontSize"`
DragLocked *bool `json:"dragLocked"`
CarOrder *[]string `json:"carOrder"`
ChargerTabOrder *[]string `json:"chargerTabOrder"`
ChargerCardOrder *[]string `json:"chargerCardOrder"`
}
// maxCarOrder bounds the stored arrangement. listCars fetches at most 200 owned
@@ -168,6 +173,10 @@ func normalizeChargerTabOrder(in []string) ([]string, error) {
return normalizeOrder("chargerTabOrder", in, maxChargerTabOrder)
}
func normalizeChargerCardOrder(in []string) ([]string, error) {
return normalizeOrder("chargerCardOrder", in, maxChargerTabOrder)
}
func normalizeOrder(field string, in []string, max int) ([]string, error) {
if len(in) > max {
return nil, fmt.Errorf("%s is too long (max %d)", field, max)
@@ -284,6 +293,14 @@ func (s *Server) handleUpdateMe(w http.ResponseWriter, r *http.Request) {
}
payload["charger_tab_order"] = keys
}
if in.ChargerCardOrder != nil {
keys, err := normalizeChargerCardOrder(*in.ChargerCardOrder)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
payload["charger_card_order"] = keys
}
var rec userRecord
if err := s.pb.Update(r.Context(), s.usersCollection(), claims.ID, payload, &rec); err != nil {
+2
View File
@@ -255,6 +255,8 @@ var collectionsSchema = map[string][]fieldDef{
// The charging page's tab order. Per user like the garage order, and for
// the same reason: it is this person's arrangement of their own page.
fJSON("charger_tab_order", 2000),
// The charging page's card order, stored the same way as its tab order.
fJSON("charger_card_order", 2000),
},
}
+3 -2
View File
@@ -495,8 +495,9 @@ type User struct {
// CarOrder is the garage arrangement: car ids in the order this user dragged
// them into. The car list is already returned in this order, so a client only
// needs it to send an updated arrangement back.
CarOrder []string `json:"carOrder"`
ChargerTabOrder []string `json:"chargerTabOrder"`
CarOrder []string `json:"carOrder"`
ChargerTabOrder []string `json:"chargerTabOrder"`
ChargerCardOrder []string `json:"chargerCardOrder"`
// Non-empty while an account-deletion request is pending its cooldown.
DeletionRequestedAt *time.Time `json:"deletionRequestedAt,omitempty"`