@@ -14,8 +14,8 @@ type User struct {
1414 CreatedAt time.Time `json:"created_at"`
1515}
1616
17- // GridItem represents an item (app or widget) in the layout grid
18- type GridItem struct {
17+ // GridElement represents an element (app or widget) in the layout grid
18+ type GridElement struct {
1919 Type string `json:"type"` // "app" or "widget"
2020 ID string `json:"id"` // app name or widget id
2121 Col int `json:"col"` // 1-based column position
@@ -24,12 +24,6 @@ type GridItem struct {
2424 Rowspan int `json:"rowspan"` // number of rows to span
2525}
2626
27- // Layout represents the user's home page layout
28- type Layout struct {
29- Items []GridItem `json:"items"`
30- WidgetConfigs map [string ]map [string ]any `json:"widgetConfigs"`
31- }
32-
3327// UserStore manages users in the database
3428type UserStore struct {
3529 db * sql.DB
@@ -78,204 +72,49 @@ func (s *UserStore) GetByUsername(username string) (*User, error) {
7872 return & user , nil
7973}
8074
81- // GetLayout returns the user's layout preferences
82- func (s * UserStore ) GetLayout (userID string ) (* Layout , error ) {
83- var prefsJSON []byte
75+ // GetLayout returns the user's layout as an array of grid elements
76+ func (s * UserStore ) GetLayout (userID string ) ([] GridElement , error ) {
77+ var layoutJSON []byte
8478 err := s .db .QueryRow (
85- "SELECT COALESCE(layout, '{ \" items \" :[], \" widgetConfigs \" :{}} ') FROM users WHERE id = $1" ,
79+ "SELECT COALESCE(layout, '[] ') FROM users WHERE id = $1" ,
8680 userID ,
87- ).Scan (& prefsJSON )
81+ ).Scan (& layoutJSON )
8882 if err == sql .ErrNoRows {
8983 return nil , nil
9084 }
9185 if err != nil {
9286 return nil , fmt .Errorf ("failed to get layout: %w" , err )
9387 }
9488
95- var prefs Layout
96- if err := json .Unmarshal (prefsJSON , & prefs ); err != nil {
89+ // Try to parse as array first (new format)
90+ var elements []GridElement
91+ if err := json .Unmarshal (layoutJSON , & elements ); err == nil {
92+ return elements , nil
93+ }
94+
95+ // Fall back to old format {elements: [...], widgetConfigs: {...}}
96+ var oldFormat struct {
97+ Elements []GridElement `json:"elements"`
98+ }
99+ if err := json .Unmarshal (layoutJSON , & oldFormat ); err != nil {
97100 return nil , fmt .Errorf ("failed to parse layout: %w" , err )
98101 }
99- return & prefs , nil
102+ return oldFormat . Elements , nil
100103}
101104
102- // SetLayout updates the user's layout preferences
103- func (s * UserStore ) SetLayout (userID string , prefs * Layout ) error {
104- prefsJSON , err := json .Marshal (prefs )
105+ // SetLayout updates the user's layout
106+ func (s * UserStore ) SetLayout (userID string , elements [] GridElement ) error {
107+ layoutJSON , err := json .Marshal (elements )
105108 if err != nil {
106109 return fmt .Errorf ("failed to marshal layout: %w" , err )
107110 }
108111
109112 _ , err = s .db .Exec (
110113 "UPDATE users SET layout = $1 WHERE id = $2" ,
111- prefsJSON , userID ,
114+ layoutJSON , userID ,
112115 )
113116 if err != nil {
114117 return fmt .Errorf ("failed to update layout: %w" , err )
115118 }
116119 return nil
117120}
118-
119- // AddAppToLayout adds an app to the user's layout at the next available position
120- func (s * UserStore ) AddAppToLayout (userID string , appName string ) error {
121- prefs , err := s .GetLayout (userID )
122- if err != nil {
123- return err
124- }
125- if prefs == nil {
126- prefs = & Layout {Items : []GridItem {}, WidgetConfigs : map [string ]map [string ]any {}}
127- }
128-
129- // Check if app already exists
130- for _ , item := range prefs .Items {
131- if item .Type == "app" && item .ID == appName {
132- return nil // Already exists
133- }
134- }
135-
136- // Find next available position (simple algorithm: find first empty 1x1 slot)
137- pos := findNextAvailablePosition (prefs .Items , 1 , 1 )
138-
139- prefs .Items = append (prefs .Items , GridItem {
140- Type : "app" ,
141- ID : appName ,
142- Col : pos .col ,
143- Row : pos .row ,
144- Colspan : 1 ,
145- Rowspan : 1 ,
146- })
147-
148- return s .SetLayout (userID , prefs )
149- }
150-
151- // RemoveAppFromLayout removes an app from the user's layout
152- func (s * UserStore ) RemoveAppFromLayout (userID string , appName string ) error {
153- prefs , err := s .GetLayout (userID )
154- if err != nil {
155- return err
156- }
157- if prefs == nil {
158- return nil // No prefs, nothing to remove
159- }
160-
161- // Filter out the app
162- newItems := make ([]GridItem , 0 , len (prefs .Items ))
163- for _ , item := range prefs .Items {
164- if ! (item .Type == "app" && item .ID == appName ) {
165- newItems = append (newItems , item )
166- }
167- }
168- prefs .Items = newItems
169-
170- return s .SetLayout (userID , prefs )
171- }
172-
173- // position helper for grid placement
174- type position struct {
175- col int
176- row int
177- }
178-
179- // findNextAvailablePosition finds the next available grid position for an item
180- func findNextAvailablePosition (items []GridItem , colspan , rowspan int ) position {
181- const gridCols = 6
182-
183- // Find max row currently in use
184- maxRow := 0
185- for _ , item := range items {
186- endRow := item .Row + item .Rowspan - 1
187- if endRow > maxRow {
188- maxRow = endRow
189- }
190- }
191-
192- // Search for available space row by row
193- for row := 1 ; row <= maxRow + 10 ; row ++ {
194- for col := 1 ; col <= gridCols - colspan + 1 ; col ++ {
195- canPlace := true
196- for c := col ; c < col + colspan && canPlace ; c ++ {
197- for r := row ; r < row + rowspan && canPlace ; r ++ {
198- if isCellOccupied (items , c , r ) {
199- canPlace = false
200- }
201- }
202- }
203- if canPlace {
204- return position {col : col , row : row }
205- }
206- }
207- }
208-
209- // Fallback: place at end
210- return position {col : 1 , row : maxRow + 1 }
211- }
212-
213- // isCellOccupied checks if a cell is occupied by any item
214- func isCellOccupied (items []GridItem , col , row int ) bool {
215- for _ , item := range items {
216- endCol := item .Col + item .Colspan - 1
217- endRow := item .Row + item .Rowspan - 1
218- if col >= item .Col && col <= endCol && row >= item .Row && row <= endRow {
219- return true
220- }
221- }
222- return false
223- }
224-
225- // AddAppToAllUsersLayouts adds an app to all users' layouts
226- // Called when an app is installed
227- func (s * UserStore ) AddAppToAllUsersLayouts (appName string ) error {
228- // Get all user IDs
229- rows , err := s .db .Query ("SELECT id FROM users" )
230- if err != nil {
231- return fmt .Errorf ("failed to get users: %w" , err )
232- }
233- defer rows .Close ()
234-
235- var userIDs []string
236- for rows .Next () {
237- var id string
238- if err := rows .Scan (& id ); err != nil {
239- return fmt .Errorf ("failed to scan user id: %w" , err )
240- }
241- userIDs = append (userIDs , id )
242- }
243-
244- // Add app to each user's layout
245- for _ , userID := range userIDs {
246- if err := s .AddAppToLayout (userID , appName ); err != nil {
247- return fmt .Errorf ("failed to add app to user %s layout: %w" , userID , err )
248- }
249- }
250-
251- return nil
252- }
253-
254- // RemoveAppFromAllUsersLayouts removes an app from all users' layouts
255- // Called when an app is uninstalled
256- func (s * UserStore ) RemoveAppFromAllUsersLayouts (appName string ) error {
257- // Get all user IDs
258- rows , err := s .db .Query ("SELECT id FROM users" )
259- if err != nil {
260- return fmt .Errorf ("failed to get users: %w" , err )
261- }
262- defer rows .Close ()
263-
264- var userIDs []string
265- for rows .Next () {
266- var id string
267- if err := rows .Scan (& id ); err != nil {
268- return fmt .Errorf ("failed to scan user id: %w" , err )
269- }
270- userIDs = append (userIDs , id )
271- }
272-
273- // Remove app from each user's layout
274- for _ , userID := range userIDs {
275- if err := s .RemoveAppFromLayout (userID , appName ); err != nil {
276- return fmt .Errorf ("failed to remove app from user %s layout: %w" , userID , err )
277- }
278- }
279-
280- return nil
281- }
0 commit comments