1import { supabaseConfig } from "./supabase-config.js"; 2 3const baseHeaders = { 4 apikey: supabaseConfig.publishableKey, 5 Authorization: `Bearer ${supabaseConfig.publishableKey}`, 6 "Content-Type": "application/json" 7}; 8 9function endpoint(path, params = {}) { 10 const url = new URL(`${supabaseConfig.url}/rest/v1/${path}`); 11 Object.entries(params).forEach(([key, value]) => url.searchParams.set(key, value)); 12 return url.toString(); 13} 14 15function functionEndpoint(name) { 16 return `${supabaseConfig.url}/functions/v1/${name}`; 17} 18 19function storageObjectUrl(path) { 20 return `${supabaseConfig.url}/storage/v1/object/platform-media/${path.split("/").map(encodeURIComponent).join("/")}`; 21} 22 23function publicStorageObjectUrl(path) { 24 return `${supabaseConfig.url}/storage/v1/object/platform-public/${path.split("/").map(encodeURIComponent).join("/")}`; 25} 26 27function publicStorageUrl(path) { 28 return `${supabaseConfig.url}/storage/v1/object/public/platform-public/${path.split("/").map(encodeURIComponent).join("/")}`; 29} 30 31function storageSignUrl(path) { 32 return `${supabaseConfig.url}/storage/v1/object/sign/platform-media/${path.split("/").map(encodeURIComponent).join("/")}`; 33} 34 35export function storageReference(path) { 36 return `storage://platform-media/${path}`; 37} 38 39export function storagePathFromReference(reference) { 40 if (!reference?.startsWith("storage://platform-media/")) return null; 41 return reference.replace("storage://platform-media/", ""); 42} 43 44export async function createSignedPlaybackUrl(accessToken, reference, expiresIn = 3600) { 45 const path = storagePathFromReference(reference); 46 if (!path) return reference; 47 48 const response = await fetch(storageSignUrl(path), { 49 method: "POST", 50 headers: { 51 apikey: supabaseConfig.publishableKey, 52 Authorization: `Bearer ${accessToken}`, 53 "Content-Type": "application/json" 54 }, 55 body: JSON.stringify({ expiresIn }) 56 }); 57 58 const payload = await response.json(); 59 if (!response.ok) { 60 throw new Error(payload.message || payload.error || `Playback link failed: ${response.status}`); 61 } 62 63 const signedUrl = payload.signedURL || payload.signedUrl; 64 if (!signedUrl) throw new Error("Supabase did not return a playback link."); 65 if (signedUrl.startsWith("http")) return signedUrl; 66 if (signedUrl.startsWith("/storage/v1/")) return `${supabaseConfig.url}${signedUrl}`; 67 if (signedUrl.startsWith("/object/")) return `${supabaseConfig.url}/storage/v1${signedUrl}`; 68 return `${supabaseConfig.url}/storage/v1/${signedUrl.replace(/^\/+/, "")}`; 69} 70 71export async function uploadStorageFile(accessToken, file, path) { 72 const response = await fetch(storageObjectUrl(path), { 73 method: "POST", 74 headers: { 75 apikey: supabaseConfig.publishableKey, 76 Authorization: `Bearer ${accessToken}`, 77 "Content-Type": file.type || "application/octet-stream", 78 "x-upsert": "true" 79 }, 80 body: file 81 }); 82 83 let payload = null; 84 try { 85 payload = await response.json(); 86 } catch { 87 payload = null; 88 } 89 90 if (!response.ok) { 91 throw new Error(payload?.message || payload?.error || `Upload failed: ${response.status}`); 92 } 93 94 return { 95 path, 96 reference: storageReference(path), 97 payload 98 }; 99} 100
101export async function uploadPublicStorageFile(accessToken, file, path) { 102 const response = await fetch(publicStorageObjectUrl(path), { 103 method: "POST", 104 headers: { 105 apikey: supabaseConfig.publishableKey, 106 Authorization: `Bearer ${accessToken}`, 107 "Content-Type": file.type || "application/octet-stream", 108 "x-upsert": "true" 109 }, 110 body: file 111 }); 112 113 let payload = null; 114 try { 115 payload = await response.json(); 116 } catch { 117 payload = null; 118 } 119 120 if (!response.ok) { 121 throw new Error(payload?.message || payload?.error || `Cover image upload failed: ${response.status}`); 122 } 123 124 return { 125 path, 126 url: publicStorageUrl(path), 127 payload 128 }; 129} 130 131export async function fetchPublishedOfferings() { 132 const response = await fetch(endpoint("offerings", { 133 select: "*", 134 status: "eq.published", 135 order: "sort_order.asc" 136 }), { 137 headers: baseHeaders 138 }); 139 140 if (!response.ok) { 141 throw new Error(`Supabase offerings request failed: ${response.status}`); 142 } 143 144 return response.json(); 145} 146 147export async function signUpWithPassword(email, password, fullName, options = {}) { 148 const response = await fetch(`${supabaseConfig.url}/auth/v1/signup`, { 149 method: "POST", 150 headers: baseHeaders, 151 body: JSON.stringify({ 152 email, 153 password, 154 gotrue_meta_security: { captcha_token: options.captchaToken }, 155 data: { 156 full_name: fullName 157 } 158 }) 159 }); 160 161 const payload = await response.json(); 162 if (!response.ok) { 163 throw new Error(payload.msg || payload.message || "Signup failed"); 164 } 165 return payload; 166} 167 168export async function signInWithPassword(email, password, options = {}) { 169 const response = await fetch(`${supabaseConfig.url}/auth/v1/token?grant_type=password`, { 170 method: "POST", 171 headers: baseHeaders, 172 body: JSON.stringify({ email, password, gotrue_meta_security: { captcha_token: options.captchaToken } }), 173 signal: options.signal 174 }); 175 176 const payload = await response.json(); 177 if (!response.ok) { 178 throw new Error(payload.error_description || payload.msg || payload.message || "Login failed"); 179 } 180 return payload; 181} 182 183export async function refreshSession(refreshToken) { 184 const response = await fetch(`${supabaseConfig.url}/auth/v1/token?grant_type=refresh_token`, { 185 method: "POST", 186 headers: baseHeaders, 187 body: JSON.stringify({ refresh_token: refreshToken }) 188 }); 189 190 const payload = await response.json(); 191 if (!response.ok) { 192 throw new Error(payload.error_description || payload.msg || payload.message || "Login refresh failed"); 193 } 194 return payload; 195} 196 197export async function sendPasswordRecovery(email, redirectTo, options = {}) { 198 const response = await fetch(`${supabaseConfig.url}/auth/v1/recover`, { 199 method: "POST", 200 headers: baseHeaders, 201 body: JSON.stringify({ 202 email, 203 redirect_to: redirectTo, 204 gotrue_meta_security: { captcha_token: options.captchaToken } 205 }) 206 }); 207 208 const text = await response.text(); 209 let payload = {}; 210 try { 211 payload = text ? JSON.parse(text) || {} : {}; 212 } catch { 213 payload = {}; 214 } 215 if (!response.ok) { 216 throw new Error(payload.msg || payload.message || "Password recovery failed"); 217 } 218 return payload; 219} 220 221export async function updatePassword(accessToken, password) { 222 const response = await fetch(`${supabaseConfig.url}/auth/v1/user`, { 223 method: "PUT", 224 headers: { 225 ...baseHeaders, 226 Authorization: `Bearer ${accessToken}` 227 }, 228 body: JSON.stringify({ password }) 229 }); 230 231 const payload = await response.json(); 232 if (!response.ok) { 233 throw new Error(payload.msg || payload.message || "Password update failed"); 234 } 235 return payload; 236} 237 238export async function fetchProfile(accessToken, userId) { 239 const response = await fetch(endpoint("profiles", { 240 select: "*", 241 id: `eq.${userId}`, 242 limit: "1" 243 }), { 244 headers: { 245 ...baseHeaders, 246 Authorization: `Bearer ${accessToken}` 247 } 248 }); 249 250 if (!response.ok) { 251 throw new Error(`Profile request failed: ${response.status}`); 252 } 253 254 const rows = await response.json(); 255 return rows[0] || null; 256} 257 258export async function fetchProfiles(accessToken) { 259 const response = await fetch(endpoint("profiles", { 260 select: "*", 261 order: "created_at.desc" 262 }), { 263 headers: { 264 ...baseHeaders, 265 Authorization: `Bearer ${accessToken}` 266 } 267 }); 268 269 if (!response.ok) { 270 throw new Error(`Profiles request failed: ${response.status}`); 271 } 272 273 return response.json(); 274} 275 276export async function findProfileByEmail(accessToken, email) { 277 const response = await fetch(endpoint("profiles", { 278 select: "*", 279 email: `eq.${email}`, 280 limit: "1" 281 }), { 282 headers: { 283 ...baseHeaders, 284 Authorization: `Bearer ${accessToken}` 285 } 286 }); 287 288 if (!response.ok) { 289 throw new Error(`Profile lookup failed: ${response.status}`); 290 } 291 292 const rows = await response.json(); 293 return rows[0] || null; 294} 295 296export async function fetchEnrollments(accessToken) { 297 const response = await fetch(endpoint("enrollments", { 298 select: "*", 299 order: "created_at.desc" 300 }), { 301 headers: { 302 ...baseHeaders, 303 Authorization: `Bearer ${accessToken}` 304 } 305 }); 306 307 if (!response.ok) { 308 throw new Error(`Enrollments request failed: ${response.status}`); 309 } 310 311 return response.json(); 312} 313 314export async function fetchCourseProgress(accessToken, courseSlug, userId = "") { 315 const query = { 316 select: "*", 317 course_slug: `eq.${courseSlug}`, 318 limit: "1" 319 }; 320 if (userId) query.user_id = `eq.${userId}`; 321 322 const response = await fetch(endpoint("course_progress", query), { 323 headers: { 324 ...baseHeaders, 325 Authorization: `Bearer ${accessToken}` 326 } 327 }); 328 329 const payload = await response.json(); 330 if (!response.ok) { 331 throw new Error(payload.message || `Course progress request failed: ${response.status}`); 332 } 333 334 return payload[0] || null; 335} 336 337export async function fetchAllCourseProgress(accessToken) { 338 const response = await fetch(endpoint("course_progress", { 339 select: "*", 340 order: "updated_at.desc" 341 }), { 342 headers: { 343 ...baseHeaders, 344 Authorization: `Bearer ${accessToken}` 345 } 346 }); 347 348 const payload = await response.json(); 349 if (!response.ok) { 350 throw new Error(payload.message || `Course progress request failed: ${response.status}`); 351 } 352 353 return payload; 354} 355 356export async function upsertCourseProgress(accessToken, progress) { 357 const response = await fetch(endpoint("course_progress", {
358 on_conflict: "user_id,course_slug" 359 }), { 360 method: "POST", 361 headers: { 362 ...baseHeaders, 363 Authorization: `Bearer ${accessToken}`, 364 Prefer: "resolution=merge-duplicates,return=representation" 365 }, 366 body: JSON.stringify({ 367 user_id: progress.userId, 368 course_slug: progress.courseSlug, 369 completed_days: progress.completedDays, 370 selected_path: progress.selectedPath || null, 371 last_route: progress.lastRoute, 372 updated_at: new Date().toISOString() 373 }) 374 }); 375 376 const payload = await response.json(); 377 if (!response.ok) { 378 throw new Error(payload.message || `Course progress save failed: ${response.status}`); 379 } 380 381 return payload[0] || null; 382} 383 384export async function fetchPayments(accessToken) { 385 const response = await fetch(endpoint("payments", { 386 select: "*", 387 order: "created_at.desc" 388 }), { 389 headers: { 390 ...baseHeaders, 391 Authorization: `Bearer ${accessToken}` 392 } 393 }); 394 395 if (!response.ok) { 396 throw new Error(`Payments request failed: ${response.status}`); 397 } 398 399 return response.json(); 400} 401 402export async function createStripeCheckoutSession(accessToken, offeringId) { 403 const response = await fetch(functionEndpoint("create-checkout-session"), { 404 method: "POST", 405 headers: { 406 ...baseHeaders, 407 Authorization: `Bearer ${accessToken}` 408 }, 409 body: JSON.stringify({ offeringId }) 410 }); 411 412 const text = await response.text(); 413 let payload = {}; 414 try { 415 payload = text ? JSON.parse(text) || {} : {}; 416 } catch { 417 payload = { error: text }; 418 } 419 if (!response.ok) { 420 throw new Error(payload.error || payload.message || `Stripe checkout failed: ${response.status}`); 421 } 422 return payload; 423} 424 425async function parseFunctionResponse(response, fallbackMessage) { 426 const text = await response.text(); 427 const contentType = response.headers.get("content-type") || ""; 428 const isHtml = /text\/html/i.test(contentType) || /^\s*<!doctype html/i.test(text); 429 let payload = {}; 430 try { 431 payload = text ? JSON.parse(text) || {} : {}; 432 } catch { 433 payload = {}; 434 } 435 if (!response.ok) { 436 if (isHtml) { 437 throw new Error("We couldnât complete that request just now. Please refresh the page and try again."); 438 } 439 throw new Error(payload.error || payload.message || `${fallbackMessage}: ${response.status}`); 440 } 441 return payload; 442} 443 444export async function createPayPalOrder(accessToken, offeringId) { 445 const response = await fetch(functionEndpoint("create-paypal-order"), { 446 method: "POST", 447 headers: { 448 ...baseHeaders, 449 Authorization: `Bearer ${accessToken}` 450 }, 451 body: JSON.stringify({ offeringId }) 452 }); 453 454 return parseFunctionResponse(response, "PayPal checkout failed"); 455} 456 457export async function requestGiftAccess(accessToken, offeringId) { 458 const response = await fetch("/api/free-course-access", { 459 method: "POST", 460 headers: { 461 "Content-Type": "application/json", 462 Authorization: `Bearer ${accessToken}` 463 }, 464 body: JSON.stringify({ offeringId }) 465 }); 466 467 return parseFunctionResponse(response, "Free access request failed"); 468} 469 470export async function redeemCoupon(accessToken, offeringId, code) { 471 const response = await fetch("/api/redeem-coupon", { 472 method: "POST", 473 headers: { 474 "Content-Type": "application/json", 475 Authorization: `Bearer ${accessToken}` 476 }, 477 body: JSON.stringify({ offeringId, code }) 478 }); 479 480 return parseFunctionResponse(response, "Coupon redemption failed"); 481} 482 483export async function capturePayPalOrder(accessToken, orderId, offeringId) { 484 const response = await fetch(functionEndpoint("capture-paypal-order"), { 485 method: "POST", 486 headers: { 487 ...baseHeaders, 488 Authorization: `Bearer ${accessToken}` 489 }, 490 body: JSON.stringify({ orderId, offeringId }) 491 }); 492 493 return parseFunctionResponse(response, "PayPal confirmation failed"); 494} 495 496export async function importWordPressAudio(accessToken, items) { 497 const response = await fetch(functionEndpoint("import-wordpress-audio"), { 498 method: "POST", 499 headers: { 500 ...baseHeaders, 501 Authorization: `Bearer ${accessToken}` 502 }, 503 body: JSON.stringify({ items }) 504 }); 505 506 return parseFunctionResponse(response, "WordPress audio import failed"); 507} 508 509export async function fetchLessonsForCourse(courseId) { 510 const response = await fetch(endpoint("lessons", { 511 select: "*", 512 course_id: `eq.${courseId}`, 513 status: "eq.published", 514 order: "order_index.asc" 515 }), { 516 headers: baseHeaders 517 }); 518 519 if (!response.ok) { 520 throw new Error(`Lessons request failed: ${response.status}`); 521 } 522 523 return response.json(); 524} 525
526export async function fetchMeditationDetails() { 527 const response = await fetch(endpoint("meditation_details", { 528 select: "*" 529 }), { 530 headers: baseHeaders 531 }); 532 533 if (!response.ok) { 534 throw new Error(`Meditation details request failed: ${response.status}`); 535 } 536 537 return response.json(); 538} 539 540export async function fetchMediaAssets(accessToken) { 541 const response = await fetch(endpoint("media_assets", { 542 select: "*", 543 order: "created_at.desc" 544 }), { 545 headers: { 546 ...baseHeaders, 547 Authorization: `Bearer ${accessToken}` 548 } 549 }); 550 551 const payload = await response.json(); 552 if (!response.ok) { 553 throw new Error(payload.message || `Media library request failed: ${response.status}`); 554 } 555 556 return payload; 557} 558 559export async function createOffering(accessToken, offering) { 560 const response = await fetch(endpoint("offerings"), { 561 method: "POST", 562 headers: { 563 ...baseHeaders, 564 Authorization: `Bearer ${accessToken}`, 565 Prefer: "return=representation" 566 }, 567 body: JSON.stringify({ 568 type: offering.type, 569 title: offering.title, 570 slug: offering.slug, 571 short_description: offering.shortDescription, 572 description: offering.description, 573 price_cents: offering.priceCents, 574 currency: "usd", 575 access_rule: offering.accessRule, 576 status: offering.status || "published", 577 featured_image_url: offering.image, 578 is_featured: false, 579 sort_order: offering.sortOrder || 999 580 }) 581 }); 582 583 const payload = await response.json(); 584 if (!response.ok) { 585 throw new Error(payload.message || `Create offering failed: ${response.status}`); 586 } 587 return payload[0]; 588} 589 590export async function updateOffering(accessToken, offeringId, offering) { 591 const response = await fetch(endpoint("offerings", { 592 id: `eq.${offeringId}` 593 }), { 594 method: "PATCH", 595 headers: { 596 ...baseHeaders, 597 Authorization: `Bearer ${accessToken}`, 598 Prefer: "return=representation" 599 }, 600 body: JSON.stringify({ 601 type: offering.type, 602 title: offering.title, 603 slug: offering.slug, 604 short_description: offering.shortDescription, 605 description: offering.description, 606 price_cents: offering.priceCents, 607 access_rule: offering.accessRule, 608 status: offering.status, 609 featured_image_url: offering.image, 610 updated_at: new Date().toISOString() 611 }) 612 }); 613 614 const payload = await response.json(); 615 if (!response.ok) { 616 throw new Error(payload.message || `Update offering failed: ${response.status}`); 617 } 618 return payload[0]; 619} 620 621export async function deleteOffering(accessToken, offeringId) { 622 const response = await fetch(endpoint("offerings", { 623 id: `eq.${offeringId}` 624 }), { 625 method: "DELETE", 626 headers: { 627 ...baseHeaders, 628 Authorization: `Bearer ${accessToken}`, 629 Prefer: "return=representation" 630 } 631 }); 632 633 const payload = await response.json(); 634 if (!response.ok) { 635 throw new Error(payload.message || `Delete offering failed: ${response.status}`); 636 } 637 return payload[0] || null; 638} 639 640export async function createEnrollment(accessToken, enrollment) { 641 const response = await fetch(endpoint("enrollments"), { 642 method: "POST", 643 headers: { 644 ...baseHeaders, 645 Authorization: `Bearer ${accessToken}`, 646 Prefer: "return=representation" 647 }, 648 body: JSON.stringify({ 649 user_id: enrollment.userId, 650 offering_id: enrollment.offeringId, 651 source: enrollment.source, 652 payment_id: enrollment.paymentId || null, 653 granted_by_admin_id: enrollment.grantedByAdminId || null, 654 admin_note: enrollment.note || null 655 }) 656 }); 657 658 const payload = await response.json(); 659 if (!response.ok) { 660 if (payload.code === "23505" || /duplicate key|enrollments_user_id_offering_id_key/i.test(payload.message || "")) { 661 const error = new Error("This student already has access to this offering."); 662 error.code = "DUPLICATE_ENROLLMENT"; 663 throw error; 664 } 665 throw new Error(payload.message || `Grant access failed: ${response.status}`); 666 } 667 return payload[0]; 668} 669 670export async function restoreEnrollment(accessToken, enrollmentId, enrollment) { 671 const response = await fetch(endpoint("enrollments", { id: `eq.${enrollmentId}` }), { 672 method: "PATCH", 673 headers: { 674 ...baseHeaders, 675 Authorization: `Bearer ${accessToken}`, 676 Prefer: "return=representation" 677 }, 678 body: JSON.stringify({ 679 source: enrollment.source, 680 payment_id: enrollment.paymentId || null, 681 granted_by_admin_id: enrollment.grantedByAdminId || null, 682 admin_note: enrollment.note || null, 683 revoked_at: null 684 }) 685 }); 686 687 const payload = await response.json(); 688 if (!response.ok) { 689 throw new Error(payload.message || `Restore access failed: ${response.status}`); 690 } 691 return payload[0]; 692} 693 694export async function revokeEnrollment(accessToken, enrollmentId) { 695 const response = await fetch(endpoint("enrollments", { id: `eq.${enrollmentId}` }), { 696 method: "PATCH", 697 headers: { 698 ...baseHeaders, 699 Authorization: `Bearer ${accessToken}`, 700 Prefer: "return=representation" 701 }, 702 body: JSON.stringify({ 703 revoked_at: new Date().toISOString() 704 }) 705 }); 706 707 const payload = await response.json(); 708 if (!response.ok) { 709 throw new Error(payload.message || `Revoke access failed: ${response.status}`); 710 } 711 return payload[0]; 712} 713 714export async function createLesson(accessToken, lesson) { 715 const response = await fetch(endpoint("lessons"), { 716 method: "POST", 717 headers: { 718 ...baseHeaders, 719 Authorization: `Bearer ${accessToken}`, 720 Prefer: "return=representation" 721 }, 722 body: JSON.stringify({ 723 course_id: lesson.courseId, 724 title: lesson.title, 725 slug: lesson.slug, 726 order_index: lesson.orderIndex, 727 content: lesson.content, 728 audio_url: lesson.audioUrl || null, 729 video_url: lesson.videoUrl || null, 730 status: "published" 731 }) 732 }); 733 734 const payload = await response.json(); 735 if (!response.ok) { 736 throw new Error(payload.message || `Create lesson failed: ${response.status}`); 737 } 738 return payload[0]; 739} 740 741export async function updateLesson(accessToken, lessonId, lesson) { 742 const response = await fetch(endpoint("lessons", { 743 id: `eq.${lessonId}` 744 }), { 745 method: "PATCH", 746 headers: { 747 ...baseHeaders, 748 Authorization: `Bearer ${accessToken}`, 749 Prefer: "return=representation" 750 }, 751 body: JSON.stringify({ 752 title: lesson.title, 753 slug: lesson.slug, 754 order_index: lesson.orderIndex, 755 content: lesson.content, 756 audio_url: lesson.audioUrl || null, 757 video_url: lesson.videoUrl || null, 758 status: lesson.status || "published", 759 updated_at: new Date().toISOString() 760 }) 761 }); 762 763 const payload = await response.json(); 764 if (!response.ok) { 765 throw new Error(payload.message || `Update lesson failed: ${response.status}`); 766 } 767 return payload[0]; 768} 769 770export async function deleteLesson(accessToken, lessonId) { 771 const response = await fetch(endpoint("lessons", { 772 id: `eq.${lessonId}` 773 }), { 774 method: "DELETE", 775 headers: { 776 ...baseHeaders, 777 Authorization: `Bearer ${accessToken}`, 778 Prefer: "return=representation" 779 } 780 }); 781 782 const payload = await response.json(); 783 if (!response.ok) { 784 throw new Error(payload.message || `Delete lesson failed: ${response.status}`); 785 } 786 return payload[0] || null; 787} 788 789export async function createMediaAsset(accessToken, asset) { 790 const response = await fetch(endpoint("media_assets"), { 791 method: "POST", 792 headers: { 793 ...baseHeaders, 794 Authorization: `Bearer ${accessToken}`, 795 Prefer: "return=representation" 796 }, 797 body: JSON.stringify({ 798 title: asset.title, 799 kind: asset.kind, 800 provider: asset.provider, 801 file_url: asset.fileUrl, 802 preview_url: asset.previewUrl || null, 803 duration_seconds: asset.durationSeconds || null, 804 transcript: asset.transcript || null, 805 notes: asset.notes || null, 806 created_by_admin_id: asset.createdByAdminId || null 807 }) 808 }); 809 810 const payload = await response.json(); 811 if (!response.ok) { 812 throw new Error(payload.message || `Create media asset failed: ${response.status}`); 813 } 814 return payload[0]; 815} 816 817export async function deleteMediaAsset(accessToken, assetId) { 818 const response = await fetch(endpoint("media_assets", { 819 id: `eq.${assetId}` 820 }), { 821 method: "DELETE", 822 headers: { 823 ...baseHeaders, 824 Authorization: `Bearer ${accessToken}`, 825 Prefer: "return=representation" 826 } 827 }); 828 829 const payload = await response.json(); 830 if (!response.ok) { 831 throw new Error(payload.message || `Delete media asset failed: ${response.status}`); 832 } 833 return payload[0] || null; 834} 835 836export async function upsertMeditationDetail(accessToken, detail) { 837 const response = await fetch(endpoint("meditation_details", { 838 on_conflict: "offering_id" 839 }), { 840 method: "POST", 841 headers: { 842 ...baseHeaders, 843 Authorization: `Bearer ${accessToken}`, 844 Prefer: "resolution=merge-duplicates,return=representation" 845 }, 846 body: JSON.stringify({ 847 offering_id: detail.offeringId, 848 audio_url: detail.audioUrl || null, 849 preview_audio_url: detail.previewAudioUrl || null, 850 duration_seconds: detail.durationSeconds || null, 851 practice_notes: detail.practiceNotes || null 852 }) 853 }); 854 855 const payload = await response.json(); 856 if (!response.ok) { 857 throw new Error(payload.message || `Save meditation detail failed: ${response.status}`); 858 } 859 return payload[0]; 860} 861 862export async function deleteMeditationDetail(accessToken, offeringId) { 863 const response = await fetch(endpoint("meditation_details", { 864 offering_id: `eq.${offeringId}` 865 }), { 866 method: "DELETE", 867 headers: { 868 ...baseHeaders, 869 Authorization: `Bearer ${accessToken}`, 870 Prefer: "return=representation" 871 } 872 }); 873 874 const payload = await response.json(); 875 if (!response.ok) { 876 throw new Error(payload.message || `Delete meditation detail failed: ${response.status}`); 877 } 878 return payload[0] || null; 879} 880 881export async function fetchAuthUser(accessToken) { 882 const response = await fetch(`${supabaseConfig.url}/auth/v1/user`, { 883 headers: { 884 ...baseHeaders, 885 Authorization: `Bearer ${accessToken}` 886 } 887 }); 888 889 const payload = await response.json(); 890 if (!response.ok) { 891 throw new Error(payload.msg || payload.message || "Could not load confirmed user"); 892 } 893 return payload; 894} 895 896export function mapSupabaseOffering(row) { 897 return { 898 id: row.id, 899 type: row.type, 900 title: row.title, 901 slug: row.slug, 902 priceCents: row.price_cents, 903 accessRule: row.access_rule, 904 status: row.status, 905 image: row.featured_image_url,
906 category: row.type === "course" ? "Course" : "Meditation", 907 duration: row.type === "course" ? "Lessons" : "Audio", 908 lessonCount: undefined, 909 description: row.description || row.short_description || "" 910 }; 911}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.