diff --git a/modules/rcal/components/folk-calendar-view.ts b/modules/rcal/components/folk-calendar-view.ts index dcdc630..ea6492f 100644 --- a/modules/rcal/components/folk-calendar-view.ts +++ b/modules/rcal/components/folk-calendar-view.ts @@ -565,6 +565,48 @@ class FolkCalendarView extends HTMLElement { // ── MONTH +8 ── { start: rel(8, 8, 10, 0), durationMin: 480, title: "FOSDEM", source: 3, desc: "Free & Open Source Developers European Meeting", location: "ULB, Brussels", virtual: false, lat: 50.8120, lng: 4.3817, breadcrumb: "Earth > Europe > Belgium > Brussels", likelihood: 25 }, { start: rel(8, 9, 10, 0), durationMin: 480, title: "FOSDEM Day 2", source: 3, desc: "Decentralization devroom", location: "ULB, Brussels", virtual: false, lat: 50.8120, lng: 4.3817, breadcrumb: "Earth > Europe > Belgium > Brussels", likelihood: 25 }, + + // ── PLANETARY / COSMIC SCALE — visible when zoomed all the way out ── + { start: new Date(year, 2, 20, 15, 6), durationMin: 1, title: "March Equinox", source: 2, desc: "Vernal equinox \u2014 day and night equal length. Marks astronomical spring in Northern Hemisphere.", location: "Earth", virtual: false, breadcrumb: "Earth" }, + { start: new Date(year, 5, 20, 22, 51), durationMin: 1, title: "June Solstice", source: 2, desc: "Summer solstice \u2014 longest day in Northern Hemisphere, shortest in Southern.", location: "Earth", virtual: false, breadcrumb: "Earth" }, + { start: new Date(year, 8, 22, 18, 19), durationMin: 1, title: "September Equinox", source: 2, desc: "Autumnal equinox \u2014 equal day and night. Marks astronomical fall.", location: "Earth", virtual: false, breadcrumb: "Earth" }, + { start: new Date(year, 11, 21, 15, 3), durationMin: 1, title: "December Solstice", source: 2, desc: "Winter solstice \u2014 shortest day in Northern Hemisphere. Yule, Dongzhi.", location: "Earth", virtual: false, breadcrumb: "Earth" }, + { start: new Date(year, 3, 22, 0, 0), durationMin: 1440, title: "Earth Day", source: 4, desc: "Global day of environmental action \u2014 1 billion+ participants across 192 countries.", location: "Earth", virtual: false, breadcrumb: "Earth" }, + { start: new Date(year, 7, 12, 0, 0), durationMin: 360, title: "Perseid Meteor Shower Peak", source: 2, desc: "Up to 100 meteors/hour visible. Best viewing after midnight in dark skies.", location: "Northern Hemisphere", virtual: false, breadcrumb: "Earth" }, + { start: new Date(year, 9, 14, 0, 0), durationMin: 1440, title: "International Repair Day", source: 4, desc: "Global movement for right to repair \u2014 events in 50+ countries.", location: "Earth", virtual: false, breadcrumb: "Earth" }, + + // ── CONTINENTAL SCALE — visible at continent zoom ── + { start: new Date(year, 4, 9, 0, 0), durationMin: 1440, title: "Europe Day", source: 4, desc: "Celebrating peace and unity across the European continent. Open-door events at EU institutions.", location: "Europe", virtual: false, lat: 50.0, lng: 10.0, breadcrumb: "Earth > Europe" }, + { start: new Date(year, 8, 16, 0, 0), durationMin: 10080, title: "European Mobility Week", source: 4, desc: "Week-long car-free zones, bike lanes, and public transport events across 3,000+ European cities.", location: "Europe", virtual: false, lat: 48.5, lng: 9.0, breadcrumb: "Earth > Europe", likelihood: 80 }, + { start: new Date(year, 10, 1, 0, 0), durationMin: 1440, title: "Pan-European Climate Strike", source: 4, desc: "Coordinated Fridays for Future actions across European capitals \u2014 Berlin, Paris, Brussels, Madrid, Stockholm.", location: "Europe", virtual: false, lat: 50.0, lng: 10.0, breadcrumb: "Earth > Europe", likelihood: 60 }, + + // ── COUNTRY SCALE — visible at country zoom ── + { start: new Date(year, 9, 3, 0, 0), durationMin: 1440, title: "German Unity Day", source: 2, desc: "National holiday celebrating German reunification (1990). Public celebrations in all 16 federal states.", location: "Germany", virtual: false, lat: 51.1657, lng: 10.4515, breadcrumb: "Earth > Europe > Germany" }, + { start: new Date(year, 5, 21, 0, 0), durationMin: 1440, title: "F\u00eate de la Musique", source: 2, desc: "Free live music in every town and city across France \u2014 streets, parks, and courtyards.", location: "France", virtual: false, lat: 46.6034, lng: 1.8883, breadcrumb: "Earth > Europe > France" }, + { start: new Date(year, 0, 1, 0, 0), durationMin: 1440, title: "Neujahrstag", source: 2, desc: "New Year's Day \u2014 national holiday across Germany.", location: "Germany", virtual: false, lat: 51.1657, lng: 10.4515, breadcrumb: "Earth > Europe > Germany" }, + + // ── BIOREGION / REGION SCALE ── + { start: new Date(year, 5, 1, 0, 0), durationMin: 129600, title: "Brandenburg Summer Festival Season", source: 4, desc: "Three months of open-air concerts, garden fests, and lake events across the Brandenburg lakes region.", location: "Brandenburg", virtual: false, lat: 52.4085, lng: 12.5316, breadcrumb: "Earth > Europe > Germany > Brandenburg", likelihood: 70 }, + { start: new Date(year, 8, 1, 10, 0), durationMin: 2880, title: "Rh\u00f6n Biosphere Reserve Hiking Week", source: 2, desc: "Guided hikes through Germany\u2019s Land of Open Distances \u2014 wetlands, beech forests, stargazing.", location: "Rh\u00f6n Biosphere Reserve", virtual: false, lat: 50.4900, lng: 9.9700, breadcrumb: "Earth > Europe > Germany > Hessen > Rh\u00f6n", likelihood: 45 }, + { start: new Date(year, 4, 15, 0, 0), durationMin: 4320, title: "Amsterdam Maker Festival", source: 3, desc: "3-day festival of open hardware, creative tech, and community fabrication across Amsterdam Noord.", location: "NDSM Wharf, Amsterdam", virtual: false, lat: 52.4012, lng: 4.8921, breadcrumb: "Earth > Europe > Netherlands > Amsterdam > Noord", likelihood: 55 }, + + // ── CITY SCALE — visible at city zoom (already well covered, adding a few unique ones) ── + { start: new Date(year, 6, 1, 0, 0), durationMin: 43200, title: "48h Neuk\u00f6lln Art Festival", source: 4, desc: "Annual open-studio art festival across 500+ venues in Neuk\u00f6lln. Galleries, squats, caf\u00e9s.", location: "Neuk\u00f6lln, Berlin", virtual: false, lat: 52.4812, lng: 13.4350, breadcrumb: "Earth > Europe > Germany > Berlin > Neuk\u00f6lln" }, + { start: new Date(year, 7, 1, 0, 0), durationMin: 20160, title: "Kreuzberg Community Garden Season", source: 4, desc: "Prinzessinneng\u00e4rten open hours \u2014 weekly workshops on composting, seed saving, urban agriculture.", location: "Prinzessinneng\u00e4rten, Kreuzberg", virtual: false, lat: 52.4939, lng: 13.4115, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, + + // ── MULTI-CONTINENT — showing global reach ── + { start: rel(2, 1, 0, 0), durationMin: 7200, title: "Open Source Summit \u2014 Tokyo", source: 3, desc: "5-day Linux Foundation summit. Keynotes, kernel track, supply chain security.", location: "Tokyo Big Sight", virtual: false, lat: 35.6320, lng: 139.7970, breadcrumb: "Earth > Asia > Japan > Tokyo", likelihood: 35 }, + { start: rel(3, 10, 0, 0), durationMin: 5760, title: "RightsCon \u2014 Nairobi", source: 3, desc: "Global summit on human rights in the digital age. 4 days, 3000+ participants.", location: "KICC, Nairobi", virtual: false, lat: -1.2864, lng: 36.8172, breadcrumb: "Earth > Africa > Kenya > Nairobi", likelihood: 30 }, + { start: rel(4, 20, 0, 0), durationMin: 4320, title: "Cumbre de los Pueblos \u2014 Bogot\u00e1", source: 4, desc: "People\u2019s summit on commons-based governance in Latin America.", location: "Universidad Nacional, Bogot\u00e1", virtual: false, lat: 4.6371, lng: -74.0836, breadcrumb: "Earth > Americas > Colombia > Bogot\u00e1", likelihood: 25 }, + { start: rel(5, 5, 0, 0), durationMin: 2880, title: "IndiaFOSS 4.0", source: 3, desc: "India\u2019s largest free software conference. Bangalore campus.", location: "NIMHANS, Bangalore", virtual: false, lat: 12.9437, lng: 77.5960, breadcrumb: "Earth > Asia > India > Karnataka > Bangalore", likelihood: 20 }, + + // ── SEASON-SCALE events — visible when temporal zoom = Season ── + { start: new Date(year, 3, 1, 0, 0), durationMin: 131040, title: "Q2 \u2014 Local-First Sync Engine", source: 0, desc: "Quarter goal: ship Automerge-based sync with offline conflict resolution. Milestones: Week 2 prototype, Week 8 beta, Week 12 production.", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin", likelihood: 80 }, + { start: new Date(year, 6, 1, 0, 0), durationMin: 131040, title: "Q3 \u2014 Federation & Multi-Space", source: 0, desc: "Quarter goal: cross-space document sharing, ActivityPub integration, provider mesh network.", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin", likelihood: 60 }, + + // ── YEAR-SCALE events — visible when temporal zoom = Year ── + { start: new Date(year, 0, 1, 0, 0), durationMin: 525600, title: "2026 Vision: Cosmolocal Infrastructure", source: 0, desc: "Annual objective: build production-grade cosmolocal infrastructure stack \u2014 identity, commerce, governance, and media tools for community self-organization.", location: null, virtual: true, breadcrumb: "Earth" }, + { start: new Date(year, 0, 1, 0, 0), durationMin: 525600, title: "Permacomputing Transition Year", source: 4, desc: "Year-long community commitment to reduce digital infrastructure footprint by 30%. Monthly audits, quarterly reports.", location: null, virtual: true, breadcrumb: "Earth > Europe" }, ]; this.events = demoEvents.map((e, i) => { diff --git a/modules/rcal/mod.ts b/modules/rcal/mod.ts index 9adeaf5..caccf51 100644 --- a/modules/rcal/mod.ts +++ b/modules/rcal/mod.ts @@ -245,6 +245,7 @@ function seedDemoIfEmpty(space: string) { attendees: [], attendeeCount: 0, metadata: null, + likelihood: null, createdAt: now, updatedAt: now, }; @@ -384,6 +385,7 @@ routes.post("/api/events", async (c) => { attendees: [], attendeeCount: 0, metadata: metadata, + likelihood: null, createdAt: now, updatedAt: now, }; diff --git a/modules/rcart/components/folk-group-buy-page.ts b/modules/rcart/components/folk-group-buy-page.ts new file mode 100644 index 0000000..712ca25 --- /dev/null +++ b/modules/rcart/components/folk-group-buy-page.ts @@ -0,0 +1,371 @@ +/** + * — Public shareable group buy page. + * + * Attributes: space, buy-id + * Fetches GET /api/group-buys/:id, polls every 10s. + * Shows product hero, tier progress, pledge panel. + * Demo mode when buyId starts with 'demo-'. + */ + +class FolkGroupBuyPage extends HTMLElement { + private shadow: ShadowRoot; + private space = 'default'; + private buyId = ''; + private data: any = null; + private loading = true; + private error = ''; + private pollTimer: ReturnType | null = null; + private pledgeQty = 1; + private pledgeName = ''; + private pledging = false; + private pledged = false; + + constructor() { + super(); + this.shadow = this.attachShadow({ mode: 'open' }); + } + + connectedCallback() { + this.space = this.getAttribute('space') || 'default'; + this.buyId = this.getAttribute('buy-id') || ''; + this.loadData(); + this.startPolling(); + } + + disconnectedCallback() { + this.stopPolling(); + } + + private getApiBase(): string { + const path = window.location.pathname; + const match = path.match(/^(\/[^/]+)?\/rcart/); + return match ? match[0] : '/rcart'; + } + + private async loadData() { + this.loading = true; + this.render(); + + if (this.buyId.startsWith('demo-')) { + this.data = this.getDemoData(); + this.loading = false; + this.render(); + return; + } + + try { + const res = await fetch(`${this.getApiBase()}/api/group-buys/${this.buyId}`); + if (!res.ok) throw new Error('Group buy not found'); + this.data = await res.json(); + } catch (e) { + this.error = e instanceof Error ? e.message : 'Failed to load group buy'; + } + this.loading = false; + this.render(); + } + + private startPolling() { + this.pollTimer = setInterval(async () => { + if (!this.buyId || this.buyId.startsWith('demo-')) return; + try { + const res = await fetch(`${this.getApiBase()}/api/group-buys/${this.buyId}`); + if (res.ok) { + const updated = await res.json(); + if (updated.totalPledged !== this.data?.totalPledged) { + this.data = updated; + this.render(); + } + } + } catch { /* silent poll fail */ } + }, 10000); + } + + private stopPolling() { + if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; } + } + + private getDemoData() { + return { + id: this.buyId, + title: 'Cosmolocal Network Tee', + productType: 'tee', + imageUrl: '/images/catalog/catalog-cosmolocal-tee.jpg', + description: 'Bella+Canvas 3001 tee with the Cosmolocal Network radial design in teal and coral.', + tiers: [ + { min_qty: 1, per_unit: 25, currency: 'USD' }, + { min_qty: 10, per_unit: 21.25, currency: 'USD' }, + { min_qty: 25, per_unit: 18, currency: 'USD' }, + { min_qty: 50, per_unit: 15, currency: 'USD' }, + ], + status: 'OPEN', + totalPledged: 17, + currentTier: { min_qty: 10, per_unit: 21.25, currency: 'USD' }, + pledges: [ + { id: 'p1', displayName: 'Alice', quantity: 5, pledgedAt: new Date(Date.now() - 86400000 * 3).toISOString() }, + { id: 'p2', displayName: 'Bob', quantity: 3, pledgedAt: new Date(Date.now() - 86400000 * 2).toISOString() }, + { id: 'p3', displayName: 'Cooperative Hub', quantity: 6, pledgedAt: new Date(Date.now() - 86400000).toISOString() }, + { id: 'p4', displayName: 'Dana', quantity: 3, pledgedAt: new Date(Date.now() - 3600000).toISOString() }, + ], + closesAt: new Date(Date.now() + 86400000 * 14).toISOString(), + createdAt: new Date(Date.now() - 86400000 * 5).toISOString(), + }; + } + + private async submitPledge() { + if (this.pledging || this.pledgeQty < 1) return; + + if (this.buyId.startsWith('demo-')) { + this.data.totalPledged += this.pledgeQty; + this.data.pledges.push({ + id: `p-${Date.now()}`, + displayName: this.pledgeName || 'Anonymous', + quantity: this.pledgeQty, + pledgedAt: new Date().toISOString(), + }); + // Recalculate current tier + const tiers = this.data.tiers; + this.data.currentTier = [...tiers].reverse().find((t: any) => this.data.totalPledged >= t.min_qty) || tiers[0]; + this.pledged = true; + this.render(); + return; + } + + this.pledging = true; + this.render(); + try { + const res = await fetch(`${this.getApiBase()}/api/group-buys/${this.buyId}/pledge`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(localStorage.getItem('encryptid-token') ? { 'Authorization': `Bearer ${localStorage.getItem('encryptid-token')}` } : {}), + }, + body: JSON.stringify({ quantity: this.pledgeQty, displayName: this.pledgeName || 'Anonymous' }), + }); + if (res.ok) { + this.pledged = true; + await this.loadData(); + } + } catch (e) { + console.error('Failed to pledge:', e); + } + this.pledging = false; + this.render(); + } + + private render() { + const styles = this.getStyles(); + + if (this.loading) { + this.shadow.innerHTML = `
Loading group buy...
`; + return; + } + if (this.error) { + this.shadow.innerHTML = `
${this.esc(this.error)}
`; + return; + } + + const d = this.data; + if (!d) return; + + const nextTier = d.tiers.find((t: any) => t.min_qty > d.totalPledged); + const nextTierQty = nextTier ? nextTier.min_qty : d.tiers[d.tiers.length - 1]?.min_qty || 0; + const progressPct = nextTier ? Math.min(100, Math.round((d.totalPledged / nextTier.min_qty) * 100)) : 100; + const remaining = nextTier ? nextTier.min_qty - d.totalPledged : 0; + const closesDate = new Date(d.closesAt); + const daysLeft = Math.max(0, Math.ceil((closesDate.getTime() - Date.now()) / 86400000)); + + this.shadow.innerHTML = ` + +
+
+ ${d.imageUrl ? `${this.esc(d.title)}` : ''} +
+

${this.esc(d.title)}

+ ${d.productType ? `${this.esc(d.productType)}` : ''} + ${d.description ? `

${this.esc(d.description)}

` : ''} +
+ ${d.status} + ${daysLeft} days left + ${d.totalPledged} pledged +
+
+
+ +
+
+

Volume Pricing Tiers

+
+ ${d.tiers.map((t: any, i: number) => { + const active = d.currentTier && t.min_qty === d.currentTier.min_qty; + const reached = d.totalPledged >= t.min_qty; + const savings = i > 0 ? Math.round((1 - t.per_unit / d.tiers[0].per_unit) * 100) : 0; + return `
+ ${t.min_qty}+ + $${t.per_unit.toFixed(2)}/ea + ${savings > 0 ? `-${savings}%` : ``} + ${reached ? `` : ''} +
`; + }).join('')} +
+ + ${nextTier ? ` +
+
${remaining} more to unlock $${nextTier.per_unit.toFixed(2)}/ea
+
+
${d.totalPledged} / ${nextTierQty}
+
` : `
Best tier unlocked!
`} + +

Pledges (${d.pledges.length})

+
+ ${d.pledges.map((p: any) => ` +
+ ${this.esc(p.displayName)} + ${p.quantity} +
`).join('')} +
+
+ +
+

Join this Group Buy

+ ${this.pledged ? ` +
+
+

Pledge submitted! Share this link to bring more people in.

+ +
+ ` : ` +
+ + + +
+ + + +
+ ${d.currentTier ? `
$${(d.currentTier.per_unit * this.pledgeQty).toFixed(2)} at current tier
` : ''} + +
+ `} +
+
+
`; + + this.bindEvents(); + } + + private bindEvents() { + this.shadow.querySelector("[data-action='qty-dec']")?.addEventListener("click", () => { + if (this.pledgeQty > 1) { this.pledgeQty--; this.render(); } + }); + this.shadow.querySelector("[data-action='qty-inc']")?.addEventListener("click", () => { + this.pledgeQty++; this.render(); + }); + this.shadow.querySelector("[data-field='pledge-qty']")?.addEventListener("change", (e) => { + this.pledgeQty = Math.max(1, parseInt((e.target as HTMLInputElement).value) || 1); + this.render(); + }); + this.shadow.querySelector("[data-field='pledge-name']")?.addEventListener("input", (e) => { + this.pledgeName = (e.target as HTMLInputElement).value; + }); + this.shadow.querySelector("[data-action='submit-pledge']")?.addEventListener("click", () => { + this.submitPledge(); + }); + this.shadow.querySelector("[data-action='copy-link']")?.addEventListener("click", () => { + navigator.clipboard.writeText(window.location.href); + const btn = this.shadow.querySelector("[data-action='copy-link']") as HTMLElement; + if (btn) { btn.textContent = 'Copied!'; setTimeout(() => { btn.textContent = 'Copy Share Link'; }, 2000); } + }); + } + + private getStyles(): string { + return ` + :host { display: block; padding: 1.5rem; max-width: 900px; margin: 0 auto; } + * { box-sizing: border-box; } + + .loading { text-align: center; padding: 3rem; color: var(--rs-text-secondary); } + .error { background: rgba(239,68,68,0.1); border: 1px solid var(--rs-error); border-radius: 8px; padding: 1rem; color: #fca5a5; text-align: center; } + + .hero { display: flex; gap: 1.5rem; margin-bottom: 2rem; } + .hero-img { width: 200px; height: 200px; border-radius: 12px; object-fit: cover; flex-shrink: 0; } + .hero-title { color: var(--rs-text-primary); font-size: 1.75rem; font-weight: 700; margin: 0 0 0.5rem; } + .hero-desc { color: var(--rs-text-secondary); font-size: 0.9375rem; line-height: 1.5; margin: 0.5rem 0; } + .hero-meta { display: flex; gap: 1rem; align-items: center; color: var(--rs-text-muted); font-size: 0.8125rem; margin-top: 0.75rem; } + + .tag { display: inline-block; padding: 0.125rem 0.5rem; border-radius: 4px; font-size: 0.6875rem; } + .tag-type { background: rgba(99,102,241,0.1); color: var(--rs-primary-hover); } + + .status { padding: 0.125rem 0.5rem; border-radius: 999px; font-size: 0.6875rem; font-weight: 500; } + .status-open { background: rgba(34,197,94,0.15); color: #4ade80; } + .status-locked { background: rgba(251,191,36,0.15); color: #fbbf24; } + .status-ordered { background: rgba(99,102,241,0.15); color: #a5b4fc; } + .status-cancelled { background: rgba(239,68,68,0.15); color: #f87171; } + + .main-grid { display: grid; grid-template-columns: 1fr 320px; gap: 2rem; } + + h3 { color: var(--rs-text-primary); font-size: 1rem; font-weight: 600; margin: 0 0 0.75rem; } + + .tier-table { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 10px; overflow: hidden; margin-bottom: 1rem; } + .tier-row { display: flex; align-items: center; padding: 0.625rem 1rem; border-bottom: 1px solid var(--rs-border-subtle); gap: 1rem; } + .tier-row:last-child { border-bottom: none; } + .tier-current { background: rgba(99,102,241,0.1); border-left: 3px solid var(--rs-primary-hover); } + .tier-reached { } + .tier-qty { color: var(--rs-text-primary); font-weight: 600; min-width: 3rem; } + .tier-price { color: var(--rs-text-primary); flex: 1; } + .tier-savings { color: #4ade80; font-size: 0.8125rem; font-weight: 500; min-width: 3rem; } + .tier-check { color: #4ade80; font-size: 0.875rem; } + + .progress-section { margin-bottom: 1.5rem; } + .progress-label { color: var(--rs-text-primary); font-size: 0.875rem; margin-bottom: 0.5rem; } + .progress-bar { background: var(--rs-bg-surface-raised); border-radius: 999px; height: 10px; overflow: hidden; } + .progress-fill { background: linear-gradient(90deg, var(--rs-primary), #8b5cf6); height: 100%; border-radius: 999px; transition: width 0.3s; } + .progress-meta { color: var(--rs-text-muted); font-size: 0.75rem; margin-top: 0.25rem; text-align: right; } + .text-green { color: #4ade80; } + + .pledges-list { margin-bottom: 1rem; } + .pledge-row { display: flex; justify-content: space-between; padding: 0.375rem 0; border-bottom: 1px solid var(--rs-border-subtle); } + .pledge-name { color: var(--rs-text-primary); font-size: 0.875rem; } + .pledge-qty { color: var(--rs-text-secondary); font-size: 0.875rem; font-weight: 600; } + + .pledge-panel { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 12px; padding: 1.25rem; height: fit-content; position: sticky; top: 1.5rem; } + .pledge-form { display: flex; flex-direction: column; gap: 0.75rem; } + .pledge-form label { color: var(--rs-text-secondary); font-size: 0.8125rem; font-weight: 500; } + .input { padding: 0.5rem 0.75rem; border-radius: 8px; border: 1px solid var(--rs-input-border); background: var(--rs-input-bg); color: var(--rs-input-text); font-size: 0.875rem; } + .input:focus { outline: none; border-color: var(--rs-primary); } + + .qty-controls { display: flex; align-items: center; gap: 0.5rem; } + .qty-input { width: 60px; text-align: center; padding: 0.375rem; border-radius: 8px; border: 1px solid var(--rs-input-border); background: var(--rs-input-bg); color: var(--rs-input-text); font-size: 0.875rem; } + + .pledge-price { color: var(--rs-text-secondary); font-size: 0.875rem; } + + .btn { padding: 0.5rem 1rem; border-radius: 8px; border: 1px solid var(--rs-border); background: var(--rs-bg-surface); color: var(--rs-text-primary); cursor: pointer; font-size: 0.875rem; } + .btn:hover { border-color: var(--rs-border-strong); } + .btn-primary { background: var(--rs-primary-hover); border-color: var(--rs-primary); color: #fff; } + .btn-primary:hover { background: #4338ca; } + .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; } + .btn-sm { padding: 0.375rem 0.75rem; font-size: 0.8125rem; } + .btn-lg { padding: 0.75rem; font-size: 1rem; width: 100%; } + + .pledge-success { text-align: center; } + .pledge-success-icon { font-size: 2rem; color: #4ade80; margin-bottom: 0.5rem; } + .pledge-success p { color: var(--rs-text-secondary); font-size: 0.875rem; margin: 0.5rem 0 1rem; } + + @media (max-width: 768px) { + .hero { flex-direction: column; } + .hero-img { width: 100%; height: auto; aspect-ratio: 1; } + .main-grid { grid-template-columns: 1fr; } + .pledge-panel { position: static; } + } + `; + } + + private esc(s: string): string { + const d = document.createElement("div"); + d.textContent = s; + return d.innerHTML; + } +} + +customElements.define("folk-group-buy-page", FolkGroupBuyPage); diff --git a/modules/rschedule/mod.ts b/modules/rschedule/mod.ts index 632e1d1..519cc2a 100644 --- a/modules/rschedule/mod.ts +++ b/modules/rschedule/mod.ts @@ -259,6 +259,7 @@ async function executeCalendarEvent( attendees: [], attendeeCount: 0, metadata: null, + likelihood: null, createdAt: now, updatedAt: now, }; @@ -1082,6 +1083,7 @@ function syncReminderToCalendar(reminder: Reminder, space: string): string | nul attendees: [], attendeeCount: 0, metadata: null, + likelihood: null, createdAt: now, updatedAt: now, }; @@ -1690,6 +1692,7 @@ async function executeWorkflowNode( attendees: [], attendeeCount: 0, metadata: null, + likelihood: null, createdAt: now, updatedAt: now, }; diff --git a/modules/rswag/components/folk-swag-designer.ts b/modules/rswag/components/folk-swag-designer.ts index 141d873..370f1c8 100644 --- a/modules/rswag/components/folk-swag-designer.ts +++ b/modules/rswag/components/folk-swag-designer.ts @@ -289,7 +289,7 @@ class FolkSwagDesigner extends HTMLElement { }, pricing: { creator_share: "35%", community_share: "15%", provider_share: "50%" }, next_actions: [ - { action: "ingest_to_rcart", label: "Send to rCart", url: "/demo/cart" }, + { action: "ingest_to_rcart", label: "Send to rCart", url: "/demo/rcart?tab=catalog" }, { action: "edit_design", label: "Edit Design" }, { action: "save_to_rfiles", label: "Save to rFiles" }, ], @@ -537,7 +537,7 @@ class FolkSwagDesigner extends HTMLElement {
Local Print
- Send to rCart + Send to rCart
${this.esc(JSON.stringify(this.artifact, null, 2))}
diff --git a/vite.config.ts b/vite.config.ts index be06997..0a69d8e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -176,6 +176,26 @@ export default defineConfig({ }, }); + // Build group buy page component + await build({ + configFile: false, + root: resolve(__dirname, "modules/rcart/components"), + build: { + emptyOutDir: false, + outDir: resolve(__dirname, "dist/modules/rcart"), + lib: { + entry: resolve(__dirname, "modules/rcart/components/folk-group-buy-page.ts"), + formats: ["es"], + fileName: () => "folk-group-buy-page.js", + }, + rollupOptions: { + output: { + entryFileNames: "folk-group-buy-page.js", + }, + }, + }, + }); + // Build payment request (QR generator) component await build({ configFile: false,