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
dragging and stuff
Florian
4 weeks ago
0eb04ab3
061333eb
+184
-55
6 changed files
expand all
collapse all
unified
split
src
lib
EditableWebsite.svelte
cards
BaseCard
BaseCard.svelte
helper.ts
routes
[handle]
+layout.server.ts
edit
+page.server.ts
api
instagram
info
+server.ts
+78
-45
src/lib/EditableWebsite.svelte
···
8
import {
9
cardsEqual,
10
clamp,
0
11
fixCollisions,
12
setCanEdit,
13
setIsMobile,
14
-
setPositionOfNewItem
0
15
} from './helper';
16
import Profile from './Profile.svelte';
17
import type { Item } from './types';
···
187
);
188
189
let showSettings = $state(false);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
190
</script>
191
192
{#if !dev}
···
234
bind:this={container}
235
ondragover={(e) => {
236
e.preventDefault();
237
-
if (!container) return;
0
0
238
239
-
const x = e.clientX + activeDragElement.mouseDeltaX;
240
-
const y = e.clientY + activeDragElement.mouseDeltaY;
241
-
const rect = container.getBoundingClientRect();
0
0
0
0
242
243
-
let gridX = clamp(
244
-
Math.floor(((x - rect.left) / rect.width) * 4),
245
-
0,
246
-
4 - (activeDragElement.w ?? 0)
247
-
);
248
-
let gridY = Math.max(Math.floor(((y - rect.top) / rect.width) * 4), 0);
249
-
if (isMobile) {
250
-
gridX = Math.floor(gridX / 2) * 2;
251
-
gridY = Math.floor(gridY / 2) * 2;
252
}
253
-
254
-
activeDragElement.x = gridX;
255
-
activeDragElement.y = gridY;
256
}}
257
ondragend={async (e) => {
258
e.preventDefault();
259
-
if (!container) return;
260
-
261
-
const x = e.clientX + activeDragElement.mouseDeltaX;
262
-
const y = e.clientY + activeDragElement.mouseDeltaY;
263
-
const rect = container.getBoundingClientRect();
264
-
265
-
let gridX = clamp(
266
-
Math.floor(((x - rect.left) / rect.width) * 4),
267
-
0,
268
-
4 - (activeDragElement.w ?? 0)
269
-
);
270
-
let gridY = Math.max(Math.floor(((y - rect.top) / rect.width) * 4), 0);
271
-
if (isMobile) {
272
-
gridX = Math.floor(gridX / 2) * 2;
273
-
gridY = Math.floor(gridY / 2) * 2;
274
-
}
275
276
if (activeDragElement.item) {
277
if (isMobile) {
278
-
activeDragElement.item.mobileX = gridX;
279
-
activeDragElement.item.mobileY = gridY;
280
} else {
281
-
activeDragElement.item.x = gridX;
282
-
activeDragElement.item.y = gridY;
283
}
284
285
fixCollisions(items, activeDragElement.item, isMobile);
···
296
bind:item={items[i]}
297
ondelete={() => {
298
items = items.filter((it) => it !== item);
0
299
}}
300
onsetsize={(newW: number, newH: number) => {
301
if (isMobile) {
···
309
fixCollisions(items, item, isMobile);
310
}}
311
ondragstart={(e) => {
312
-
const target = e.target as HTMLDivElement;
313
activeDragElement.element = target;
314
activeDragElement.w = item.w;
315
activeDragElement.h = item.h;
316
activeDragElement.item = item;
317
318
const rect = target.getBoundingClientRect();
319
-
activeDragElement.mouseDeltaX = rect.left + margin - e.clientX;
320
activeDragElement.mouseDeltaY = rect.top - e.clientY;
0
0
321
}}
322
>
323
<EditingCard bind:item={items[i]} />
···
325
{/each}
326
327
{#if activeDragElement.element && activeDragElement.x >= 0 && activeDragElement.item}
328
-
{@const item = activeDragElement}
329
<div
330
-
class={['bg-base-500/10 absolute aspect-square rounded-2xl']}
331
-
style={`translate: calc(${(item.x / 4) * 100}cqw + ${margin / 2}px) calc(${(item.y / 4) * 100}cqw + ${margin / 2}px);
332
-
333
-
width: calc(${(getW(activeDragElement.item) / 4) * 100}cqw - ${margin}px);
334
-
height: calc(${(getH(activeDragElement.item) / 4) * 100}cqw - ${margin}px);`}
0
0
0
0
0
0
0
0
0
335
></div>
336
{/if}
337
<div style="height: {((maxHeight + 1) / 4) * 100}cqw;"></div>
···
8
import {
9
cardsEqual,
10
clamp,
11
+
compactItems,
12
fixCollisions,
13
setCanEdit,
14
setIsMobile,
15
+
setPositionOfNewItem,
16
+
simulateFinalPosition
17
} from './helper';
18
import Profile from './Profile.svelte';
19
import type { Item } from './types';
···
189
);
190
191
let showSettings = $state(false);
192
+
193
+
let debugPoint = $state({ x: 0, y: 0 });
194
+
195
+
function getDragXY(
196
+
e: DragEvent & {
197
+
currentTarget: EventTarget & HTMLDivElement;
198
+
}
199
+
) {
200
+
if (!container) return;
201
+
202
+
const x = e.clientX + activeDragElement.mouseDeltaX;
203
+
const y = e.clientY + activeDragElement.mouseDeltaY;
204
+
205
+
const rect = container.getBoundingClientRect();
206
+
207
+
debugPoint.x = x - rect.left;
208
+
debugPoint.y = y - rect.top + margin;
209
+
console.log(rect.top);
210
+
211
+
let gridX = clamp(
212
+
Math.floor(((x - rect.left) / rect.width) * 4),
213
+
0,
214
+
4 - (activeDragElement.w ?? 0)
215
+
);
216
+
let gridY = Math.max(Math.round(((y - rect.top + margin) / (rect.width - margin)) * 4), 0);
217
+
if (isMobile) {
218
+
gridX = Math.floor(gridX / 2) * 2;
219
+
gridY = Math.floor(gridY / 2) * 2;
220
+
}
221
+
return { x: gridX, y: gridY };
222
+
}
223
</script>
224
225
{#if !dev}
···
267
bind:this={container}
268
ondragover={(e) => {
269
e.preventDefault();
270
+
271
+
const cell = getDragXY(e);
272
+
if (!cell) return;
273
274
+
activeDragElement.x = cell.x;
275
+
activeDragElement.y = cell.y;
276
+
277
+
// Auto-scroll when dragging near top or bottom of viewport
278
+
const scrollZone = 150;
279
+
const scrollSpeed = 15;
280
+
const viewportHeight = window.innerHeight;
281
282
+
if (e.clientY < scrollZone) {
283
+
// Near top - scroll up
284
+
const intensity = 1 - e.clientY / scrollZone;
285
+
window.scrollBy(0, -scrollSpeed * intensity);
286
+
} else if (e.clientY > viewportHeight - scrollZone) {
287
+
// Near bottom - scroll down
288
+
const intensity = 1 - (viewportHeight - e.clientY) / scrollZone;
289
+
window.scrollBy(0, scrollSpeed * intensity);
0
290
}
0
0
0
291
}}
292
ondragend={async (e) => {
293
e.preventDefault();
294
+
const cell = getDragXY(e);
295
+
if (!cell) return;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
296
297
if (activeDragElement.item) {
298
if (isMobile) {
299
+
activeDragElement.item.mobileX = cell.x;
300
+
activeDragElement.item.mobileY = cell.y;
301
} else {
302
+
activeDragElement.item.x = cell.x;
303
+
activeDragElement.item.y = cell.y;
304
}
305
306
fixCollisions(items, activeDragElement.item, isMobile);
···
317
bind:item={items[i]}
318
ondelete={() => {
319
items = items.filter((it) => it !== item);
320
+
compactItems(items, isMobile);
321
}}
322
onsetsize={(newW: number, newH: number) => {
323
if (isMobile) {
···
331
fixCollisions(items, item, isMobile);
332
}}
333
ondragstart={(e) => {
334
+
const target = e.currentTarget as HTMLDivElement;
335
activeDragElement.element = target;
336
activeDragElement.w = item.w;
337
activeDragElement.h = item.h;
338
activeDragElement.item = item;
339
340
const rect = target.getBoundingClientRect();
341
+
activeDragElement.mouseDeltaX = rect.left - e.clientX;
342
activeDragElement.mouseDeltaY = rect.top - e.clientY;
343
+
console.log(activeDragElement.mouseDeltaY);
344
+
console.log(rect.width);
345
}}
346
>
347
<EditingCard bind:item={items[i]} />
···
349
{/each}
350
351
{#if activeDragElement.element && activeDragElement.x >= 0 && activeDragElement.item}
352
+
{@const finalPos = simulateFinalPosition(items, activeDragElement.item, activeDragElement.x, activeDragElement.y, isMobile)}
353
<div
354
+
class={[
355
+
'bg-base-500/10 absolute aspect-square rounded-2xl transition-transform duration-100'
356
+
]}
357
+
style={`translate: calc(${(finalPos.x / 4) * 100}cqw + ${margin}px) calc(${(finalPos.y / 4) * 100}cqw + ${margin}px);
358
+
359
+
width: calc(${(getW(activeDragElement.item) / 4) * 100}cqw - ${margin * 2}px);
360
+
height: calc(${(getH(activeDragElement.item) / 4) * 100}cqw - ${margin * 2}px);`}
361
+
></div>
362
+
{/if}
363
+
364
+
{#if dev}
365
+
<div
366
+
class="absolute size-4 rounded-full bg-red-500"
367
+
style={`translate: ${debugPoint.x}px ${debugPoint.y}px;`}
368
></div>
369
{/if}
370
<div style="height: {((maxHeight + 1) / 4) * 100}cqw;"></div>
+1
-1
src/lib/cards/BaseCard/BaseCard.svelte
···
66
height: calc((var(--mh) / 8) * 100cqw - (var(--mm) * 2));
67
}
68
69
-
@container wrapper (width >= 64rem) {
70
.card {
71
translate: calc((var(--dx) / 8) * 100cqw + var(--dm))
72
calc((var(--dy) / 8) * 100cqw + var(--dm));
···
66
height: calc((var(--mh) / 8) * 100cqw - (var(--mm) * 2));
67
}
68
69
+
@container grid (width >= 42rem) {
70
.card {
71
translate: calc((var(--dx) / 8) * 100cqw + var(--dm))
72
calc((var(--dy) / 8) * 100cqw + var(--dm));
+71
src/lib/helper.ts
···
94
if (mobile) it.mobileX = clamp(it.mobileX, 0, COLS - it.mobileW);
95
else it.x = clamp(it.x, 0, COLS - it.w);
96
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
97
}
98
99
export function sortItems(a: Item, b: Item) {
···
94
if (mobile) it.mobileX = clamp(it.mobileX, 0, COLS - it.mobileW);
95
else it.x = clamp(it.x, 0, COLS - it.w);
96
}
97
+
98
+
compactItems(items, mobile);
99
+
}
100
+
101
+
// Move all items up as far as possible without collisions
102
+
export function compactItems(items: Item[], mobile: boolean = false) {
103
+
// Sort by Y position (top-to-bottom) so upper items settle first.
104
+
const sortedItems = items.toSorted((a, b) =>
105
+
mobile ? a.mobileY - b.mobileY || a.mobileX - b.mobileX : a.y - b.y || a.x - b.x
106
+
);
107
+
108
+
for (const item of sortedItems) {
109
+
// Try moving item up row by row until we hit y=0 or a collision
110
+
while (true) {
111
+
const currentY = mobile ? item.mobileY : item.y;
112
+
if (currentY <= 0) break;
113
+
114
+
// Temporarily move up by 1
115
+
if (mobile) item.mobileY -= 1;
116
+
else item.y -= 1;
117
+
118
+
// Check for collision with any other item
119
+
const hasCollision = items.some((other) => other !== item && overlaps(item, other, mobile));
120
+
121
+
if (hasCollision) {
122
+
// Revert the move
123
+
if (mobile) item.mobileY += 1;
124
+
else item.y += 1;
125
+
break;
126
+
}
127
+
// No collision, keep the new position and try moving up again
128
+
}
129
+
}
130
+
}
131
+
132
+
// Simulate where an item would end up after fixCollisions + compaction
133
+
export function simulateFinalPosition(
134
+
items: Item[],
135
+
movedItem: Item,
136
+
newX: number,
137
+
newY: number,
138
+
mobile: boolean = false
139
+
): { x: number; y: number } {
140
+
// Deep clone positions for simulation
141
+
const clonedItems: Item[] = items.map((item) => ({
142
+
...item,
143
+
x: item.x,
144
+
y: item.y,
145
+
mobileX: item.mobileX,
146
+
mobileY: item.mobileY
147
+
}));
148
+
149
+
const clonedMovedItem = clonedItems.find((item) => item.id === movedItem.id);
150
+
if (!clonedMovedItem) return { x: newX, y: newY };
151
+
152
+
// Set the new position
153
+
if (mobile) {
154
+
clonedMovedItem.mobileX = newX;
155
+
clonedMovedItem.mobileY = newY;
156
+
} else {
157
+
clonedMovedItem.x = newX;
158
+
clonedMovedItem.y = newY;
159
+
}
160
+
161
+
// Run fixCollisions on the cloned data
162
+
fixCollisions(clonedItems, clonedMovedItem, mobile);
163
+
164
+
// Return the final position of the moved item
165
+
return mobile
166
+
? { x: clonedMovedItem.mobileX, y: clonedMovedItem.mobileY }
167
+
: { x: clonedMovedItem.x, y: clonedMovedItem.y };
168
}
169
170
export function sortItems(a: Item, b: Item) {
src/routes/[handle]/+page.server.ts
src/routes/[handle]/+layout.server.ts
-9
src/routes/[handle]/edit/+page.server.ts
···
1
-
import { loadData } from '$lib/website/load';
2
-
import { env } from '$env/dynamic/private';
3
-
import { error } from '@sveltejs/kit';
4
-
5
-
export async function load({ params }) {
6
-
if (env.PUBLIC_IS_SELFHOSTED) error(404);
7
-
const data = await loadData(params.handle);
8
-
return { ...data, handle: params.handle };
9
-
}
···
0
0
0
0
0
0
0
0
0
+34
src/routes/api/instagram/info/+server.ts
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import { json } from '@sveltejs/kit';
2
+
3
+
export async function GET({ url }) {
4
+
const username = url.searchParams.get('username');
5
+
if (!username) {
6
+
return json({ error: 'No username provided' }, { status: 400 });
7
+
}
8
+
9
+
const requestUrl = `https://i.instagram.com/api/v1/users/web_profile_info/?username=${encodeURIComponent(username)}`;
10
+
11
+
try {
12
+
const response = await fetch(requestUrl, {
13
+
headers: {
14
+
'User-Agent':
15
+
'Instagram 76.0.0.15.395 Android (24/7.0; 640dpi; 1440x2560; samsung; SM-G930F; herolte; samsungexynos8890; en_US; 138226743)',
16
+
Origin: 'https://www.instagram.com',
17
+
Referer: 'https://www.instagram.com/'
18
+
}
19
+
});
20
+
21
+
if (!response.ok) {
22
+
return json(
23
+
{ error: 'Failed to fetch Instagram profile', status: response.status },
24
+
{ status: response.status }
25
+
);
26
+
}
27
+
28
+
const data = await response.json();
29
+
return json({ follower_count: data['data']['user']['edge_followed_by']['count'] });
30
+
} catch (error) {
31
+
console.error('Error fetching Instagram profile:', error);
32
+
return json({ error: 'Failed to fetch Instagram profile' }, { status: 500 });
33
+
}
34
+
}