tangled
alpha
login
or
join now
flo-bit.dev
/
blento
your personal website on atproto - mirror
blento.app
20
fork
atom
overview
issues
pulls
pipelines
fixes
Florian
4 days ago
b97eeb52
15836064
+137
-76
3 changed files
expand all
collapse all
unified
split
src
lib
helper.ts
website
EditableWebsite.svelte
load.ts
+33
-16
src/lib/helper.ts
···
180
180
mobile ? a.mobileY - b.mobileY || a.mobileX - b.mobileX : a.y - b.y || a.x - b.x
181
181
);
182
182
183
183
+
// For each item, find the lowest Y it can occupy by checking the bottom edges
184
184
+
// of all horizontally-overlapping items already placed above it.
185
185
+
const settled: Item[] = [];
186
186
+
183
187
for (const item of sortedItems) {
184
184
-
// Try moving item up row by row until we hit y=0 or a collision
185
185
-
while (true) {
186
186
-
const currentY = mobile ? item.mobileY : item.y;
187
187
-
if (currentY <= 0) break;
188
188
+
const itemX = mobile ? item.mobileX : item.x;
189
189
+
const itemW = mobile ? item.mobileW : item.w;
188
190
189
189
-
// Temporarily move up by 1
190
190
-
if (mobile) item.mobileY -= 1;
191
191
-
else item.y -= 1;
191
191
+
let minY = 0;
192
192
193
193
-
// Check for collision with any other item
194
194
-
const hasCollision = items.some((other) => other !== item && overlaps(item, other, mobile));
193
193
+
for (const other of settled) {
194
194
+
const otherX = mobile ? other.mobileX : other.x;
195
195
+
const otherW = mobile ? other.mobileW : other.w;
195
196
196
196
-
if (hasCollision) {
197
197
-
// Revert the move
198
198
-
if (mobile) item.mobileY += 1;
199
199
-
else item.y += 1;
200
200
-
break;
197
197
+
// Check horizontal overlap
198
198
+
if (itemX < otherX + otherW && itemX + itemW > otherX) {
199
199
+
const otherBottom = mobile ? other.mobileY + other.mobileH : other.y + other.h;
200
200
+
if (otherBottom > minY) {
201
201
+
minY = otherBottom;
202
202
+
}
201
203
}
202
202
-
// No collision, keep the new position and try moving up again
204
204
+
}
205
205
+
206
206
+
if (mobile) {
207
207
+
item.mobileY = minY;
208
208
+
} else {
209
209
+
item.y = minY;
203
210
}
211
211
+
212
212
+
settled.push(item);
204
213
}
205
214
}
206
215
···
553
562
originalPublication: string
554
563
) {
555
564
const promises = [];
565
565
+
566
566
+
// Build a lookup of original cards by ID for O(1) access
567
567
+
const originalCardsById = new Map<string, Item>();
568
568
+
for (const card of data.cards) {
569
569
+
originalCardsById.set(card.id, card);
570
570
+
}
571
571
+
556
572
// find all cards that have been updated (where items differ from originalItems)
557
573
for (let item of currentItems) {
558
558
-
const originalItem = data.cards.find((i) => cardsEqual(i, item));
574
574
+
const orig = originalCardsById.get(item.id);
575
575
+
const originalItem = orig && cardsEqual(orig, item) ? orig : undefined;
559
576
560
577
if (!originalItem) {
561
578
console.log('updated or new item', item);
+84
-39
src/lib/website/EditableWebsite.svelte
···
57
57
data.publication.preferences ??= {};
58
58
data.publication.preferences.accentColor = newAccent;
59
59
data.publication.preferences.baseColor = newBase;
60
60
+
hasUnsavedChanges = true;
60
61
data = { ...data };
61
62
}
62
63
···
68
69
// svelte-ignore state_referenced_locally
69
70
let publication = $state(JSON.stringify(data.publication));
70
71
71
71
-
// Track saved state for comparison
72
72
// svelte-ignore state_referenced_locally
73
73
-
let savedItems = $state(JSON.stringify(data.cards));
74
74
-
// svelte-ignore state_referenced_locally
75
75
-
let savedPublication = $state(JSON.stringify(data.publication));
73
73
+
let savedItemsSnapshot = JSON.stringify(data.cards);
76
74
77
75
let hasUnsavedChanges = $state(false);
78
76
77
77
+
// Detect card content and publication changes (e.g. sidebar edits)
78
78
+
// The guard ensures JSON.stringify only runs while no changes are detected yet.
79
79
+
// Once hasUnsavedChanges is true, Svelte still fires this effect on item mutations
80
80
+
// but the early return makes it effectively free.
79
81
$effect(() => {
80
80
-
if (!hasUnsavedChanges) {
81
81
-
hasUnsavedChanges =
82
82
-
JSON.stringify(items) !== savedItems ||
83
83
-
JSON.stringify(data.publication) !== savedPublication;
82
82
+
if (hasUnsavedChanges) return;
83
83
+
if (
84
84
+
JSON.stringify(items) !== savedItemsSnapshot ||
85
85
+
JSON.stringify(data.publication) !== publication
86
86
+
) {
87
87
+
hasUnsavedChanges = true;
84
88
}
85
89
});
86
90
···
137
141
let editedOn = $state(data.publication.preferences?.editedOn ?? 0);
138
142
139
143
function onLayoutChanged() {
144
144
+
hasUnsavedChanges = true;
140
145
// Set the bit for the current layout: desktop=1, mobile=2
141
146
editedOn = editedOn | (isMobile ? 2 : 1);
142
147
if (shouldMirror(editedOn)) {
···
266
271
267
272
publication = JSON.stringify(data.publication);
268
273
269
269
-
// Update saved state
270
270
-
savedItems = JSON.stringify(items);
271
271
-
savedPublication = JSON.stringify(data.publication);
274
274
+
savedItemsSnapshot = JSON.stringify(items);
275
275
+
hasUnsavedChanges = false;
272
276
273
277
saveSuccess = true;
274
278
···
558
562
}
559
563
}
560
564
565
565
+
let lastGridPos: {
566
566
+
x: number;
567
567
+
y: number;
568
568
+
swapWithId: string | null;
569
569
+
placement: string | null;
570
570
+
} | null = $state(null);
571
571
+
561
572
let debugPoint = $state({ x: 0, y: 0 });
562
573
563
574
function getGridPosition(
···
750
761
751
762
e.preventDefault();
752
763
764
764
+
// Auto-scroll near edges (always process, even if grid pos unchanged)
765
765
+
const scrollZone = 100;
766
766
+
const scrollSpeed = 10;
767
767
+
const viewportHeight = window.innerHeight;
768
768
+
769
769
+
if (touch.clientY < scrollZone) {
770
770
+
const intensity = 1 - touch.clientY / scrollZone;
771
771
+
window.scrollBy(0, -scrollSpeed * intensity);
772
772
+
} else if (touch.clientY > viewportHeight - scrollZone) {
773
773
+
const intensity = 1 - (viewportHeight - touch.clientY) / scrollZone;
774
774
+
window.scrollBy(0, scrollSpeed * intensity);
775
775
+
}
776
776
+
753
777
const result = getGridPosition(touch.clientX, touch.clientY);
754
778
if (!result || !activeDragElement.item) return;
755
779
780
780
+
// Skip redundant work if grid position hasn't changed
781
781
+
if (
782
782
+
lastGridPos &&
783
783
+
lastGridPos.x === result.x &&
784
784
+
lastGridPos.y === result.y &&
785
785
+
lastGridPos.swapWithId === result.swapWithId &&
786
786
+
lastGridPos.placement === result.placement
787
787
+
) {
788
788
+
return;
789
789
+
}
790
790
+
lastGridPos = {
791
791
+
x: result.x,
792
792
+
y: result.y,
793
793
+
swapWithId: result.swapWithId,
794
794
+
placement: result.placement
795
795
+
};
796
796
+
756
797
const draggedOrigPos = activeDragElement.originalPositions.get(activeDragElement.item.id);
757
798
758
799
// Reset all items to original positions first
···
793
834
}
794
835
795
836
fixCollisions(items, activeDragElement.item, isMobile);
796
796
-
797
797
-
// Auto-scroll near edges
798
798
-
const scrollZone = 100;
799
799
-
const scrollSpeed = 10;
800
800
-
const viewportHeight = window.innerHeight;
801
801
-
802
802
-
if (touch.clientY < scrollZone) {
803
803
-
const intensity = 1 - touch.clientY / scrollZone;
804
804
-
window.scrollBy(0, -scrollSpeed * intensity);
805
805
-
} else if (touch.clientY > viewportHeight - scrollZone) {
806
806
-
const intensity = 1 - (viewportHeight - touch.clientY) / scrollZone;
807
807
-
window.scrollBy(0, scrollSpeed * intensity);
808
808
-
}
809
837
}
810
838
811
839
function touchEnd() {
···
822
850
activeDragElement.lastPlacement = null;
823
851
}
824
852
853
853
+
lastGridPos = null;
825
854
touchDragActive = false;
826
855
}
827
856
···
1270
1299
ondragover={(e) => {
1271
1300
e.preventDefault();
1272
1301
1302
1302
+
// Auto-scroll when dragging near top or bottom of viewport (always process)
1303
1303
+
const scrollZone = 100;
1304
1304
+
const scrollSpeed = 10;
1305
1305
+
const viewportHeight = window.innerHeight;
1306
1306
+
1307
1307
+
if (e.clientY < scrollZone) {
1308
1308
+
const intensity = 1 - e.clientY / scrollZone;
1309
1309
+
window.scrollBy(0, -scrollSpeed * intensity);
1310
1310
+
} else if (e.clientY > viewportHeight - scrollZone) {
1311
1311
+
const intensity = 1 - (viewportHeight - e.clientY) / scrollZone;
1312
1312
+
window.scrollBy(0, scrollSpeed * intensity);
1313
1313
+
}
1314
1314
+
1273
1315
const result = getDragXY(e);
1274
1316
if (!result) return;
1317
1317
+
1318
1318
+
// Skip redundant work if grid position hasn't changed
1319
1319
+
if (
1320
1320
+
lastGridPos &&
1321
1321
+
lastGridPos.x === result.x &&
1322
1322
+
lastGridPos.y === result.y &&
1323
1323
+
lastGridPos.swapWithId === result.swapWithId &&
1324
1324
+
lastGridPos.placement === result.placement
1325
1325
+
) {
1326
1326
+
return;
1327
1327
+
}
1328
1328
+
lastGridPos = {
1329
1329
+
x: result.x,
1330
1330
+
y: result.y,
1331
1331
+
swapWithId: result.swapWithId,
1332
1332
+
placement: result.placement
1333
1333
+
};
1275
1334
1276
1335
activeDragElement.x = result.x;
1277
1336
activeDragElement.y = result.y;
···
1323
1382
// Now fix collisions (with compacting)
1324
1383
fixCollisions(items, activeDragElement.item, isMobile);
1325
1384
}
1326
1326
-
1327
1327
-
// Auto-scroll when dragging near top or bottom of viewport
1328
1328
-
const scrollZone = 100;
1329
1329
-
const scrollSpeed = 10;
1330
1330
-
const viewportHeight = window.innerHeight;
1331
1331
-
1332
1332
-
if (e.clientY < scrollZone) {
1333
1333
-
// Near top - scroll up
1334
1334
-
const intensity = 1 - e.clientY / scrollZone;
1335
1335
-
window.scrollBy(0, -scrollSpeed * intensity);
1336
1336
-
} else if (e.clientY > viewportHeight - scrollZone) {
1337
1337
-
// Near bottom - scroll down
1338
1338
-
const intensity = 1 - (viewportHeight - e.clientY) / scrollZone;
1339
1339
-
window.scrollBy(0, scrollSpeed * intensity);
1340
1340
-
}
1341
1385
}}
1342
1386
ondragend={async (e) => {
1343
1387
e.preventDefault();
···
1348
1392
activeDragElement.item = null;
1349
1393
activeDragElement.lastTargetId = null;
1350
1394
activeDragElement.lastPlacement = null;
1395
1395
+
lastGridPos = null;
1351
1396
return true;
1352
1397
}}
1353
1398
class={[
+20
-21
src/lib/website/load.ts
···
73
73
throw error(404);
74
74
}
75
75
76
76
-
const cards = await listRecords({ did, collection: 'app.blento.card' }).catch(() => {
77
77
-
console.error('error getting records for collection app.blento.card');
78
78
-
return [] as Awaited<ReturnType<typeof listRecords>>;
79
79
-
});
80
80
-
81
81
-
const mainPublication = await getRecord({
82
82
-
did,
83
83
-
collection: 'site.standard.publication',
84
84
-
rkey: 'blento.self'
85
85
-
}).catch(() => {
86
86
-
console.error('error getting record for collection site.standard.publication');
87
87
-
return undefined;
88
88
-
});
89
89
-
90
90
-
const pages = await listRecords({ did, collection: 'app.blento.page' }).catch(() => {
91
91
-
console.error('error getting records for collection app.blento.page');
92
92
-
return [] as Awaited<ReturnType<typeof listRecords>>;
93
93
-
});
94
94
-
95
95
-
const profile = await getDetailedProfile({ did });
76
76
+
const [cards, mainPublication, pages, profile] = await Promise.all([
77
77
+
listRecords({ did, collection: 'app.blento.card' }).catch(() => {
78
78
+
console.error('error getting records for collection app.blento.card');
79
79
+
return [] as Awaited<ReturnType<typeof listRecords>>;
80
80
+
}),
81
81
+
getRecord({
82
82
+
did,
83
83
+
collection: 'site.standard.publication',
84
84
+
rkey: 'blento.self'
85
85
+
}).catch(() => {
86
86
+
console.error('error getting record for collection site.standard.publication');
87
87
+
return undefined;
88
88
+
}),
89
89
+
listRecords({ did, collection: 'app.blento.page' }).catch(() => {
90
90
+
console.error('error getting records for collection app.blento.page');
91
91
+
return [] as Awaited<ReturnType<typeof listRecords>>;
92
92
+
}),
93
93
+
getDetailedProfile({ did })
94
94
+
]);
96
95
97
96
const cardTypes = new Set(cards.map((v) => v.value.cardType ?? '') as string[]);
98
97
const cardTypesArray = Array.from(cardTypes);
···
144
143
const stringifiedResult = JSON.stringify(result);
145
144
await cache?.put?.(handle, stringifiedResult);
146
145
147
147
-
const parsedResult = JSON.parse(stringifiedResult);
146
146
+
const parsedResult = structuredClone(result) as any;
148
147
149
148
parsedResult.publication = (
150
149
parsedResult.publications as Awaited<ReturnType<typeof listRecords>>