Search Apps Documentation Source Content File Folder Download Copy Actions Download

render.gno

14.22 Kb · 483 lines
  1package gnopendao9
  2
  3import (
  4	"strconv"
  5	"strings"
  6
  7	"gno.land/p/nt/commondao"
  8	"gno.land/p/nt/ufmt"
  9	"gno.land/p/leon/svgbtn"
 10	"gno.land/p/moul/txlink"
 11)
 12
 13// ============= RENDER =============
 14
 15func Render(path string) string {
 16	if path == "" {
 17		return renderHome()
 18	}
 19
 20	if strings.HasPrefix(path, "listing/") {
 21		idStr := strings.TrimPrefix(path, "listing/")
 22		id, _ := strconv.Atoi(idStr)
 23		return renderListing(id)
 24	}
 25
 26	if strings.HasPrefix(path, "sale/") {
 27		idStr := strings.TrimPrefix(path, "sale/")
 28		id, _ := strconv.Atoi(idStr)
 29		return renderSale(id)
 30	}
 31
 32	if path == "stats" {
 33		return renderStats()
 34	}
 35
 36	if path == "proposals" {
 37		return renderProposals()
 38	}
 39
 40	if path == "archive" {
 41		return renderArchive()
 42	}
 43
 44	if strings.HasPrefix(path, "proposal/") {
 45		idStr := strings.TrimPrefix(path, "proposal/")
 46		id, _ := strconv.Atoi(idStr)
 47		return renderProposal(uint64(id))
 48	}
 49
 50	if strings.HasPrefix(path, "archived/") {
 51		idStr := strings.TrimPrefix(path, "archived/")
 52		id, _ := strconv.Atoi(idStr)
 53		return renderArchivedProposal(uint64(id))
 54	}
 55
 56	return "Page not found"
 57}
 58
 59func renderHome() string {
 60	output := "# GNOPENSEA DAO 8\n\n"
 61	output += "Decentralized NFT Marketplace with DAO Governance\n\n"
 62	output += "Compatible **GRC-721** + **GRC-2981** (Automatic Royalties)\n\n"
 63	output += "---\n\n"
 64	output += ufmt.Sprintf("**Marketplace Address:** %s\n\n", marketplaceAddr.String())
 65	output += "---\n\n"
 66
 67	output += "## Statistics\n\n"
 68	output += ufmt.Sprintf("- **Active listings:** %d\n", GetActiveListingsCount())
 69	output += ufmt.Sprintf("- **Total sales:** %d\n", GetTotalSales())
 70	output += ufmt.Sprintf("- **Total volume:** %s\n", formatPrice(GetTotalVolume()))
 71	output += ufmt.Sprintf("- **Royalties paid:** %s\n", formatPrice(GetTotalRoyaltiesPaid()))
 72	output += ufmt.Sprintf("- **Marketplace fee:** %s\n", formatFee(marketplaceFee))
 73	output += ufmt.Sprintf("- **Balance:** %s\n\n", formatPrice(GetBalance()))
 74
 75	output += "[View detailed statistics](/r/pierre115/gnopendao9:stats)\n\n"
 76	output += "---\n\n"
 77
 78	// DAO Section
 79	output += "## DAO Governance\n\n"
 80	output += ufmt.Sprintf("**Total DAO Members:** %d\n\n", GetTotalMembers())
 81
 82	activeProposals := marketplaceDAO.ActiveProposals()
 83	if activeProposals.Size() > 0 {
 84		output += ufmt.Sprintf("**Active Proposals:** %d\n\n", activeProposals.Size())
 85		output += "[View all proposals](/r/pierre115/gnopendao9:proposals)\n\n"
 86	} else {
 87		output += "**Active Proposals:** 0\n\n"
 88		output += "*No active proposals*\n\n"
 89	}
 90
 91	finishedProposals := marketplaceDAO.FinishedProposals()
 92	output += ufmt.Sprintf("[View archive (%d)](/r/pierre115/gnopendao9:archive)\n\n", finishedProposals.Size())
 93
 94	// Vote NO button
 95	linkjoin := txlink.NewLink("JoinDAO").
 96		URL()
 97	output += svgbtn.SuccessButton(100, 30, "Join !", linkjoin) + "\n\n"
 98
 99	output += "---\n\n"
100
101	// Active listings
102	if GetActiveListingsCount() > 0 {
103		output += "## NFTs for sale\n\n"
104
105		listings.Iterate("", "", func(key string, value interface{}) bool {
106			listing := value.(*Listing)
107			if listing.Active {
108				output += renderListingPreview(listing)
109			}
110			return false
111		})
112	} else {
113		output += "## No NFTs for sale\n\n"
114		output += "*Be the first to list an NFT!*\n"
115	}
116
117	return output
118}
119
120func renderListingPreview(listing *Listing) string {
121	// Calculate breakdown
122	sellerGets, marketFee, royalty, royaltyAddr := GetRoyaltyBreakdown(listing.ListingId)
123
124	output := ufmt.Sprintf("### Listing #%d\n\n", listing.ListingId)
125	output += ufmt.Sprintf("**Token ID:** %s\n\n", listing.TokenId.String())
126	output += ufmt.Sprintf("**Price:** %s\n\n", formatPrice(listing.Price))
127
128	if royalty > 0 {
129		output += ufmt.Sprintf("Royalty: %s (%s)\n\n",
130			formatPrice(royalty),
131			formatPercentage(royalty, listing.Price))
132	}
133
134	output += ufmt.Sprintf("**Seller receives:** %s\n\n", formatPrice(sellerGets))
135	output += ufmt.Sprintf("**Market Fees:** %s\n\n", formatPrice(marketFee))
136	output += ufmt.Sprintf("**Royalty Address:** %s\n\n", royaltyAddr.String())
137	output += ufmt.Sprintf("**Marketplace Address:** %s\n\n", marketplaceAddr.String())
138	output += ufmt.Sprintf("[View details](/r/pierre115/gnopendao9:listing/%d)\n\n", listing.ListingId)
139	output += "---\n\n"
140	return output
141}
142
143func renderListing(listingId int) string {
144	listing := getListing(listingId)
145	if listing == nil {
146		return "# Listing not found"
147	}
148
149	status := "Active"
150	if !listing.Active {
151		status = "Sold/Cancelled"
152	}
153
154	output := ufmt.Sprintf("# Listing #%d - %s\n\n", listingId, status)
155
156	output += "## Details\n\n"
157	output += ufmt.Sprintf("**Token ID:** %s\n\n", listing.TokenId.String())
158	output += ufmt.Sprintf("**Price:** %s\n\n", formatPrice(listing.Price))
159	output += ufmt.Sprintf("**Seller:** `%s`\n\n", listing.Seller.String())
160	output += ufmt.Sprintf("**Listed at block:** %d\n\n", listing.ListedAt)
161
162	if listing.Active {
163		sellerGets, marketFee, royalty, royaltyAddr := GetRoyaltyBreakdown(listingId)
164
165		output += "---\n\n"
166		output += "## Price breakdown\n\n"
167		output += ufmt.Sprintf("- **Total price:** %s (100%%)\n", formatPrice(listing.Price))
168		output += ufmt.Sprintf("- **Marketplace fee (%s):** %s\n",
169			formatFee(marketplaceFee), formatPrice(marketFee))
170
171		if royalty > 0 {
172			output += ufmt.Sprintf("- **Creator royalty (%s):** %s\n",
173				formatPercentage(royalty, listing.Price), formatPrice(royalty))
174			output += ufmt.Sprintf("  - Beneficiary: `%s`\n", royaltyAddr.String())
175		} else {
176			output += "- **Royalty:** None\n"
177		}
178
179		output += ufmt.Sprintf("- **Seller receives:** %s (%s)\n\n",
180			formatPrice(sellerGets), formatPercentage(sellerGets, listing.Price))
181
182		output += "## Purchase\n\n"
183		output += "```bash\n"
184		output += "gnokey maketx call \\\n"
185		output += "  -pkgpath \"gno.land/r/pierre115/gnopendao9\" \\\n"
186		output += "  -func \"BuyNFT\" \\\n"
187		output += ufmt.Sprintf("  -args \"%d\" \\\n", listingId)
188		output += ufmt.Sprintf("  -send \"%dugnot\" \\\n", listing.Price)
189		output += "  -broadcast yourkey\n"
190		output += "```\n"
191	}
192
193	output += "\n[← Back](/r/pierre115/gnopendao9)\n"
194
195	return output
196}
197
198func renderSale(saleId int) string {
199	value, exists := sales.Get(strconv.Itoa(saleId))
200	if !exists {
201		return "# Sale not found"
202	}
203
204	sale := value.(*Sale)
205
206	output := ufmt.Sprintf("# Sale #%d\n\n", saleId)
207	output += ufmt.Sprintf("**Token ID:** %s\n\n", sale.TokenId)
208	output += ufmt.Sprintf("**Price:** %s\n\n", formatPrice(sale.Price))
209	output += ufmt.Sprintf("**Buyer:** `%s`\n\n", sale.Buyer.String())
210	output += ufmt.Sprintf("**Seller:** `%s`\n\n", sale.Seller.String())
211	output += ufmt.Sprintf("**Block:** %d\n\n", sale.SoldAt)
212
213	output += "## Distribution\n\n"
214	output += ufmt.Sprintf("- **Marketplace fee:** %s\n", formatPrice(sale.MarketplaceFee))
215
216	if sale.RoyaltyFee > 0 {
217		output += ufmt.Sprintf("- **Royalty:** %s → `%s`\n",
218			formatPrice(sale.RoyaltyFee), sale.RoyaltyReceiver.String())
219	}
220
221	sellerReceived := sale.Price - sale.MarketplaceFee - sale.RoyaltyFee
222	output += ufmt.Sprintf("- **Seller received:** %s\n", formatPrice(sellerReceived))
223
224	return output
225}
226
227func renderStats() string {
228	output := "# Marketplace Statistics\n\n"
229
230	totalVolume := GetTotalVolume()
231	totalSales := GetTotalSales()
232	totalRoyalties := GetTotalRoyaltiesPaid()
233
234	output += ufmt.Sprintf("**Total volume:** %s\n\n", formatPrice(totalVolume))
235	output += ufmt.Sprintf("**Number of sales:** %d\n\n", totalSales)
236	output += ufmt.Sprintf("**Royalties paid:** %s (%s of volume)\n\n",
237		formatPrice(totalRoyalties), formatPercentage(totalRoyalties, totalVolume))
238
239	if totalSales > 0 {
240		avgPrice := totalVolume / int64(totalSales)
241		output += ufmt.Sprintf("**Average price:** %s\n\n", formatPrice(avgPrice))
242	}
243
244	output += "\n[← Back](/r/pierre115/gnopendao9)\n"
245
246	return output
247}
248
249// ============= DAO RENDER FUNCTIONS =============
250
251func renderProposals() string {
252	output := "# Active DAO Proposals\n\n"
253
254	proposals := marketplaceDAO.ActiveProposals()
255
256	if proposals.Size() == 0 {
257		output += "*No active proposals at the moment*\n\n"
258		output += "DAO members can create proposals to:\n"
259		output += "- Approve new NFT collections\n"
260		output += "- Remove existing collections\n"
261		output += "- Update marketplace fees\n\n"
262		output += "[← Back](/r/pierre115/gnopendao9)\n"
263		return output
264	}
265
266	proposals.Iterate(0, proposals.Size(), false, func(p *commondao.Proposal) bool {
267		output += renderProposalPreview(p)
268		return false
269	})
270
271	output += "\n[← Back](/r/pierre115/gnopendao9)\n"
272
273	return output
274}
275
276func renderProposalPreview(p *commondao.Proposal) string {
277	output := ufmt.Sprintf("## Proposal #%d\n\n", p.ID())
278	output += ufmt.Sprintf("**%s**\n\n", p.Definition().Title())
279
280	// Count votes
281	yesVotes := 0
282	noVotes := 0
283	p.VotingRecord().Iterate(0, p.VotingRecord().Size(), false, func(v commondao.Vote) bool {
284		if string(v.Choice) == "yes" {
285			yesVotes++
286		} else if string(v.Choice) == "no" {
287			noVotes++
288		}
289		return false
290	})
291
292	output += ufmt.Sprintf("**Yes:** %d | **No:** %d | **Total:** %d\n\n", yesVotes, noVotes, yesVotes+noVotes)
293
294	if p.HasVotingDeadlinePassed() {
295		output += "⏰ **Voting ended**\n\n"
296	} else {
297		output += "✅ **Voting open**\n\n"
298	}
299
300	output += ufmt.Sprintf("[View & Vote](/r/pierre115/gnopendao9:proposal/%d)\n\n", p.ID())
301	output += "---\n\n"
302
303	return output
304}
305
306func renderProposal(proposalID uint64) string {
307	proposal := marketplaceDAO.ActiveProposals().Get(proposalID)
308	if proposal == nil {
309		return "# Proposal not found"
310	}
311
312	output := ufmt.Sprintf("# Proposal #%d\n\n", proposalID)
313	output += ufmt.Sprintf("## %s\n\n", proposal.Definition().Title())
314	output += ufmt.Sprintf("%s\n\n", proposal.Definition().Body())
315
316	output += "---\n\n"
317
318	// Vote counts
319	yesVotes := 0
320	noVotes := 0
321	totalVotes := 0
322
323	proposal.VotingRecord().Iterate(0, proposal.VotingRecord().Size(), false, func(v commondao.Vote) bool {
324		if string(v.Choice) == "yes" {
325			yesVotes++
326		} else if string(v.Choice) == "no" {
327			noVotes++
328		}
329		totalVotes++
330		return false
331	})
332
333	output += "## Current Results\n\n"
334	output += ufmt.Sprintf("- **Yes votes:** %d\n", yesVotes)
335	output += ufmt.Sprintf("- **No votes:** %d\n", noVotes)
336	output += ufmt.Sprintf("- **Total votes:** %d\n", totalVotes)
337
338	totalMembers := GetTotalMembers()
339	quorumRequired := (totalMembers * QUORUM) / 100
340	output += ufmt.Sprintf("- **Quorum required:** %d/%d votes\n\n", totalVotes, quorumRequired)
341
342	if proposal.HasVotingDeadlinePassed() {
343		output += "⏰ **Voting period has ended**\n\n"
344	} else {
345		output += "✅ **Voting is open**\n\n"
346
347		output += "---\n\n"
348		output += "## Cast Your Vote\n\n"
349
350		// Vote YES button
351		linkyes := txlink.NewLink("Vote").
352			AddArgs("proposalID", ufmt.Sprintf("%d", proposalID)).
353			AddArgs("choice", "yes").
354			URL()
355		output += svgbtn.SuccessButton(100, 30, "YES", linkyes) + "\n\n"
356
357
358		// Vote NO button
359		linkno := txlink.NewLink("Vote").
360			AddArgs("proposalID", ufmt.Sprintf("%d", proposalID)).
361			AddArgs("choice", "no").
362			URL()
363		output += svgbtn.DangerButton(100, 30, "NO", linkno) + "\n\n"
364	}
365
366	output += "---\n\n"
367	output += "[← Back to proposals](/r/pierre115/gnopendao9:proposals) | [← Home](/r/pierre115/gnopendao9)\n"
368
369	return output
370}
371
372
373// ============= ARCHIVE RENDER FUNCTIONS =============
374
375func renderArchive() string {
376	output := "# Proposal Archive\n\n"
377
378	finishedProposals := marketplaceDAO.FinishedProposals()
379
380	if finishedProposals.Size() == 0 {
381		output += "*No finished proposals yet*\n\n"
382		output += "[← Back](/r/pierre115/gnopendao9)\n"
383		return output
384	}
385
386	output += ufmt.Sprintf("**Total archived proposals:** %d\n\n", finishedProposals.Size())
387	output += "---\n\n"
388
389	finishedProposals.Iterate(0, finishedProposals.Size(), false, func(p *commondao.Proposal) bool {
390		output += renderArchivedProposalPreview(p)
391		return false
392	})
393
394	output += "\n[← Back](/r/pierre115/gnopendao9)\n"
395
396	return output
397}
398
399func renderArchivedProposalPreview(p *commondao.Proposal) string {
400	output := ufmt.Sprintf("## Proposal #%d - %s\n\n", p.ID(), string(p.Status()))
401	output += ufmt.Sprintf("**%s**\n\n", p.Definition().Title())
402
403	// Count final votes
404	yesVotes := 0
405	noVotes := 0
406	p.VotingRecord().Iterate(0, p.VotingRecord().Size(), false, func(v commondao.Vote) bool {
407		if string(v.Choice) == "yes" || string(v.Choice) == "YES" {
408			yesVotes++
409		} else if string(v.Choice) == "no" || string(v.Choice) == "NO" {
410			noVotes++
411		}
412		return false
413	})
414
415	output += ufmt.Sprintf("**Final Result:** Yes: %d | No: %d\n\n", yesVotes, noVotes)
416
417	statusEmoji := "✅"
418	if p.Status() == "failed" {
419		statusEmoji = "❌"
420	}
421	output += ufmt.Sprintf("%s **Status:** %s\n\n", statusEmoji, string(p.Status()))
422	if p.StatusReason() != "" {
423		output += ufmt.Sprintf("**Reason:** %s\n\n", p.StatusReason())
424	}
425
426	output += ufmt.Sprintf("[View details](/r/pierre115/gnopendao9:archived/%d)\n\n", p.ID())
427	output += "---\n\n"
428
429	return output
430}
431
432func renderArchivedProposal(proposalID uint64) string {
433	proposal := marketplaceDAO.FinishedProposals().Get(proposalID)
434	if proposal == nil {
435		return "# Archived proposal not found"
436	}
437
438	output := ufmt.Sprintf("# Proposal #%d - %s\n\n", proposalID, string(proposal.Status()))
439	output += ufmt.Sprintf("## %s\n\n", proposal.Definition().Title())
440	output += ufmt.Sprintf("%s\n\n", proposal.Definition().Body())
441
442	output += "---\n\n"
443
444	// Final vote counts
445	yesVotes := 0
446	noVotes := 0
447	totalVotes := 0
448
449	proposal.VotingRecord().Iterate(0, proposal.VotingRecord().Size(), false, func(v commondao.Vote) bool {
450		if string(v.Choice) == "yes" || string(v.Choice) == "YES" {
451			yesVotes++
452		} else if string(v.Choice) == "no" || string(v.Choice) == "NO" {
453			noVotes++
454		}
455		totalVotes++
456		return false
457	})
458
459	output += "## Final Results\n\n"
460	output += ufmt.Sprintf("- **Yes votes:** %d\n", yesVotes)
461	output += ufmt.Sprintf("- **No votes:** %d\n", noVotes)
462	output += ufmt.Sprintf("- **Total votes:** %d\n", totalVotes)
463
464	totalMembers := GetTotalMembers()
465	quorumRequired := (totalMembers * QUORUM) / 100
466	output += ufmt.Sprintf("- **Quorum required:** %d/%d votes\n\n", totalVotes, quorumRequired)
467
468	statusEmoji := "✅"
469	if proposal.Status() == "failed" {
470		statusEmoji = "❌"
471	}
472	output += ufmt.Sprintf("%s **Final Status:** %s\n\n", statusEmoji, string(proposal.Status()))
473
474	if proposal.StatusReason() != "" {
475		output += ufmt.Sprintf("**Status Reason:** %s\n\n", proposal.StatusReason())
476	}
477
478	output += ufmt.Sprintf("**Voting Ended:** %s\n\n", proposal.VotingDeadline().Format("2006-01-02 15:04:05"))
479
480	output += "---\n\n"
481	output += "[← Back to archive](/r/pierre115/gnopendao9:archive) | [← Home](/r/pierre115/gnopendao9)\n"
482
483	return output
484}