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}