your personal website on atproto - mirror blento.app

Merge pull request #104 from flo-bit/various-fixes

add cards at current position (#76)

authored by Florian and committed by GitHub cda2c9ee 84984406

+187 -23
+2 -2
docs/Selfhosting.md
··· 24 24 ] 25 25 ``` 26 26 27 - 5. (maybe necessary? will improve performance at least) create your own kv store by running `npx wrangler kv namespace create USER_DATA_CACHE` and when asked add it to the `wrangler.jsonc` 27 + 5. optionally to improve performance: create your own kv store by running `npx wrangler kv namespace create USER_DATA_CACHE` and when asked add it to the `wrangler.jsonc` 28 28 29 29 DONE :) your blento should be live after a minute or two at `your-cloudflare-worker-or-custom-domain.com` and you can edit it by signing in with your bluesky account at `your-cloudflare-worker-or-custom-domain.com/edit` 30 30 31 31 6. some cards need their own additional env keys, if you have these cards in your profile, create your keys and add them to your cloudflare worker 32 32 33 - - github profile: GITHUB_TOKEN 33 + - github profile: GITHUB_TOKEN (token with public_repo access) 34 34 - map: PUBLIC_MAPBOX_TOKEN
+2 -1
src/lib/cards/GameCards/DinoGameCard/index.ts
··· 13 13 card.mobileW = 8; 14 14 card.mobileH = 6; 15 15 card.cardData = {}; 16 - } 16 + }, 17 + canHaveLabel: true 17 18 } as CardDefinition & { type: 'dino-game' };
+2 -1
src/lib/cards/GameCards/TetrisCard/index.ts
··· 18 18 card.mobileH = 12; 19 19 card.cardData = {}; 20 20 }, 21 - maxH: 10 21 + maxH: 10, 22 + canHaveLabel: true 22 23 } as CardDefinition & { type: 'tetris' };
+77 -1
src/lib/helper.ts
··· 240 240 ); 241 241 } 242 242 243 - export function setPositionOfNewItem(newItem: Item, items: Item[]) { 243 + export function setPositionOfNewItem( 244 + newItem: Item, 245 + items: Item[], 246 + viewportCenter?: { gridY: number; isMobile: boolean } 247 + ) { 248 + if (viewportCenter) { 249 + const { gridY, isMobile } = viewportCenter; 250 + 251 + if (isMobile) { 252 + // Place at viewport center Y 253 + newItem.mobileY = Math.max(0, Math.round(gridY - newItem.mobileH / 2)); 254 + newItem.mobileY = Math.floor(newItem.mobileY / 2) * 2; 255 + 256 + // Try to find a free X at this Y 257 + let found = false; 258 + for ( 259 + newItem.mobileX = 0; 260 + newItem.mobileX <= COLUMNS - newItem.mobileW; 261 + newItem.mobileX += 2 262 + ) { 263 + if (!items.some((item) => overlaps(newItem, item, true))) { 264 + found = true; 265 + break; 266 + } 267 + } 268 + if (!found) { 269 + newItem.mobileX = 0; 270 + } 271 + 272 + // Desktop: derive from mobile 273 + newItem.y = Math.max(0, Math.round(newItem.mobileY / 2)); 274 + found = false; 275 + for (newItem.x = 0; newItem.x <= COLUMNS - newItem.w; newItem.x += 2) { 276 + if (!items.some((item) => overlaps(newItem, item, false))) { 277 + found = true; 278 + break; 279 + } 280 + } 281 + if (!found) { 282 + newItem.x = 0; 283 + } 284 + } else { 285 + // Place at viewport center Y 286 + newItem.y = Math.max(0, Math.round(gridY - newItem.h / 2)); 287 + 288 + // Try to find a free X at this Y 289 + let found = false; 290 + for (newItem.x = 0; newItem.x <= COLUMNS - newItem.w; newItem.x += 2) { 291 + if (!items.some((item) => overlaps(newItem, item, false))) { 292 + found = true; 293 + break; 294 + } 295 + } 296 + if (!found) { 297 + newItem.x = 0; 298 + } 299 + 300 + // Mobile: derive from desktop 301 + newItem.mobileY = Math.max(0, Math.round(newItem.y * 2)); 302 + found = false; 303 + for ( 304 + newItem.mobileX = 0; 305 + newItem.mobileX <= COLUMNS - newItem.mobileW; 306 + newItem.mobileX += 2 307 + ) { 308 + if (!items.some((item) => overlaps(newItem, item, true))) { 309 + found = true; 310 + break; 311 + } 312 + } 313 + if (!found) { 314 + newItem.mobileX = 0; 315 + } 316 + } 317 + return; 318 + } 319 + 244 320 let foundPosition = false; 245 321 while (!foundPosition) { 246 322 for (newItem.x = 0; newItem.x <= COLUMNS - newItem.w; newItem.x++) {
+104 -18
src/lib/website/EditableWebsite.svelte
··· 129 129 130 130 let maxHeight = $derived(items.reduce((max, item) => Math.max(max, getY(item) + getH(item)), 0)); 131 131 132 + function getViewportCenterGridY(): { gridY: number; isMobile: boolean } | undefined { 133 + if (!container) return undefined; 134 + const rect = container.getBoundingClientRect(); 135 + const currentMargin = isMobile ? mobileMargin : margin; 136 + const cellSize = (rect.width - currentMargin * 2) / COLUMNS; 137 + const viewportCenterY = window.innerHeight / 2; 138 + const gridY = (viewportCenterY - rect.top - currentMargin) / cellSize; 139 + return { gridY, isMobile }; 140 + } 141 + 132 142 function newCard(type: string = 'link', cardData?: any) { 133 143 // close sidebar if open 134 144 const popover = document.getElementById('mobile-menu'); ··· 157 167 if (!newItem.item) return; 158 168 const item = newItem.item; 159 169 160 - setPositionOfNewItem(item, items); 170 + const viewportCenter = getViewportCenterGridY(); 171 + setPositionOfNewItem(item, items, viewportCenter); 161 172 162 173 items = [...items, item]; 174 + 175 + // Push overlapping items down, then compact to fill gaps 176 + fixCollisions(items, item, false, true); 177 + fixCollisions(items, item, true, true); 178 + compactItems(items, false); 179 + compactItems(items, true); 163 180 164 181 newItem = {}; 165 182 ··· 373 390 } 374 391 } 375 392 393 + function getImageDimensions(src: string): Promise<{ width: number; height: number }> { 394 + return new Promise((resolve) => { 395 + const img = new Image(); 396 + img.onload = () => resolve({ width: img.naturalWidth, height: img.naturalHeight }); 397 + img.onerror = () => resolve({ width: 1, height: 1 }); 398 + img.src = src; 399 + }); 400 + } 401 + 402 + function getBestGridSize( 403 + imageWidth: number, 404 + imageHeight: number, 405 + candidates: [number, number][] 406 + ): [number, number] { 407 + const imageRatio = imageWidth / imageHeight; 408 + let best: [number, number] = candidates[0]; 409 + let bestDiff = Infinity; 410 + 411 + for (const candidate of candidates) { 412 + const gridRatio = candidate[0] / candidate[1]; 413 + const diff = Math.abs(Math.log(imageRatio) - Math.log(gridRatio)); 414 + if (diff < bestDiff) { 415 + bestDiff = diff; 416 + best = candidate; 417 + } 418 + } 419 + 420 + return best; 421 + } 422 + 423 + const desktopSizeCandidates: [number, number][] = [ 424 + [2, 2], 425 + [2, 4], 426 + [4, 2], 427 + [4, 4], 428 + [4, 6], 429 + [6, 4] 430 + ]; 431 + const mobileSizeCandidates: [number, number][] = [ 432 + [4, 4], 433 + [4, 6], 434 + [4, 8], 435 + [6, 4], 436 + [8, 4], 437 + [8, 6] 438 + ]; 439 + 376 440 async function processImageFile(file: File, gridX?: number, gridY?: number) { 377 441 const isGif = file.type === 'image/gif'; 378 442 ··· 386 450 image: { blob: file, objectUrl } 387 451 }; 388 452 389 - // If grid position is provided 453 + // Size card based on image aspect ratio 454 + const { width, height } = await getImageDimensions(objectUrl); 455 + const [dw, dh] = getBestGridSize(width, height, desktopSizeCandidates); 456 + const [mw, mh] = getBestGridSize(width, height, mobileSizeCandidates); 457 + item.w = dw; 458 + item.h = dh; 459 + item.mobileW = mw; 460 + item.mobileH = mh; 461 + 462 + // If grid position is provided (image dropped on grid) 390 463 if (gridX !== undefined && gridY !== undefined) { 391 464 if (isMobile) { 392 465 item.mobileX = gridX; 393 466 item.mobileY = gridY; 394 - // Find valid desktop position 395 - findValidPosition(item, items, false); 467 + // Derive desktop Y from mobile 468 + item.x = Math.floor((COLUMNS - item.w) / 2); 469 + item.x = Math.floor(item.x / 2) * 2; 470 + item.y = Math.max(0, Math.round(gridY / 2)); 396 471 } else { 397 472 item.x = gridX; 398 473 item.y = gridY; 399 - // Find valid mobile position 400 - findValidPosition(item, items, true); 474 + // Derive mobile Y from desktop 475 + item.mobileX = Math.floor((COLUMNS - item.mobileW) / 2); 476 + item.mobileX = Math.floor(item.mobileX / 2) * 2; 477 + item.mobileY = Math.max(0, Math.round(gridY * 2)); 401 478 } 402 479 403 480 items = [...items, item]; 404 481 fixCollisions(items, item, isMobile); 482 + fixCollisions(items, item, !isMobile); 405 483 } else { 406 - setPositionOfNewItem(item, items); 484 + const viewportCenter = getViewportCenterGridY(); 485 + setPositionOfNewItem(item, items, viewportCenter); 407 486 items = [...items, item]; 487 + fixCollisions(items, item, false, true); 488 + fixCollisions(items, item, true, true); 489 + compactItems(items, false); 490 + compactItems(items, true); 408 491 } 409 492 410 493 await tick(); ··· 481 564 } 482 565 } 483 566 484 - for (const file of imageFiles) { 485 - await processImageFile(file, gridX, gridY); 486 - 487 - // Move to next cell position 488 - const cardW = isMobile ? 4 : 2; 489 - gridX += cardW; 490 - if (gridX + cardW > COLUMNS) { 491 - gridX = 0; 492 - gridY += isMobile ? 4 : 2; 567 + for (let i = 0; i < imageFiles.length; i++) { 568 + // First image gets the drop position, rest use normal placement 569 + if (i === 0) { 570 + await processImageFile(imageFiles[i], gridX, gridY); 571 + } else { 572 + await processImageFile(imageFiles[i]); 493 573 } 494 574 } 495 575 } ··· 537 617 objectUrl 538 618 }; 539 619 540 - setPositionOfNewItem(item, items); 620 + const viewportCenter = getViewportCenterGridY(); 621 + setPositionOfNewItem(item, items, viewportCenter); 541 622 items = [...items, item]; 623 + fixCollisions(items, item, false, true); 624 + fixCollisions(items, item, true, true); 625 + compactItems(items, false); 626 + compactItems(items, true); 542 627 543 628 await tick(); 544 629 ··· 758 843 bind:item={items[i]} 759 844 ondelete={() => { 760 845 items = items.filter((it) => it !== item); 761 - compactItems(items, isMobile); 846 + compactItems(items, false); 847 + compactItems(items, true); 762 848 }} 763 849 onsetsize={(newW: number, newH: number) => { 764 850 if (isMobile) {