Smart Contract
SemesterZero
A.ce9dd43888d99574.SemesterZero
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import MetadataViews from 0x1d7e57aa55817448
4import ViewResolver from 0x1d7e57aa55817448
5
6/// SemesterZero - Forte Hackathon Edition
7/// 3 Blockchain Features:
8/// 1. GumDrop Airdrop (72-hour claim window) - Flow Actions triggers create/close
9/// 2. Paradise Motel Day/Night (per-user timezone) - Personalized 12hr cycle using blockchain storage
10/// 3. Chapter 5 NFT Airdrop (100% completion) - Auto-airdrop on slacker + overachiever complete
11///
12/// For flunks.flow deployment - October 2025
13access(all) contract SemesterZero: NonFungibleToken, ViewResolver {
14
15 // ========================================
16 // PATHS
17 // ========================================
18
19 access(all) let UserProfileStoragePath: StoragePath
20 access(all) let UserProfilePublicPath: PublicPath
21 access(all) let Chapter5CollectionStoragePath: StoragePath
22 access(all) let Chapter5CollectionPublicPath: PublicPath
23 access(all) let AdminStoragePath: StoragePath
24
25 // ========================================
26 // EVENTS
27 // ========================================
28
29 // Contract lifecycle
30 access(all) event ContractInitialized()
31
32 // GumDrop Events (72-hour claim window)
33 access(all) event GumDropCreated(dropId: String, eligibleCount: Int, amount: UFix64, startTime: UFix64, endTime: UFix64)
34 access(all) event GumDropClaimed(user: Address, dropId: String, amount: UFix64, timestamp: UFix64)
35 access(all) event GumDropClosed(dropId: String, totalClaimed: Int, totalEligible: Int)
36
37 // Chapter 5 Events
38 access(all) event Chapter5SlackerCompleted(userAddress: Address, timestamp: UFix64)
39 access(all) event Chapter5OverachieverCompleted(userAddress: Address, timestamp: UFix64)
40 access(all) event Chapter5FullCompletion(userAddress: Address, timestamp: UFix64)
41 access(all) event Chapter5NFTMinted(nftID: UInt64, recipient: Address, timestamp: UFix64)
42 access(all) event Chapter5NFTBurned(nftID: UInt64, owner: Address, timestamp: UFix64)
43 access(all) event Withdraw(id: UInt64, from: Address?)
44 access(all) event Deposit(id: UInt64, to: Address?)
45 access(all) event Minted(id: UInt64, recipient: Address)
46
47 // ========================================
48 // STATE VARIABLES
49 // ========================================
50
51 // GumDrop system
52 access(all) var activeGumDrop: GumDrop?
53 access(all) var totalGumDrops: UInt64
54
55 // Chapter 5 tracking
56 access(all) var totalChapter5Completions: UInt64
57 access(all) var totalChapter5NFTs: UInt64
58 access(all) let chapter5Completions: {Address: Chapter5Status}
59
60 // ========================================
61 // STRUCTS
62 // ========================================
63
64 /// GumDrop - 72-hour claim window for airdrop eligibility
65 /// Flow Actions creates drop → Website shows button → User claims → Backend adds GUM to Supabase
66 /// Flow Actions closes drop after 72hrs → Website hides button
67 access(all) struct GumDrop {
68 access(all) let dropId: String
69 access(all) let amount: UFix64
70 access(all) let startTime: UFix64
71 access(all) let endTime: UFix64
72 access(all) let eligibleUsers: {Address: Bool}
73 access(all) var claimedUsers: {Address: UFix64}
74
75 init(dropId: String, amount: UFix64, eligibleUsers: [Address], durationSeconds: UFix64) {
76 self.dropId = dropId
77 self.amount = amount
78 self.startTime = getCurrentBlock().timestamp
79 self.endTime = self.startTime + durationSeconds
80 self.eligibleUsers = {}
81 self.claimedUsers = {}
82
83 for addr in eligibleUsers {
84 self.eligibleUsers[addr] = true
85 }
86 }
87
88 access(all) fun isEligible(user: Address): Bool {
89 return self.eligibleUsers[user] == true && self.claimedUsers[user] == nil
90 }
91
92 access(all) fun hasClaimed(user: Address): Bool {
93 return self.claimedUsers[user] != nil
94 }
95
96 access(all) fun isActive(): Bool {
97 let now = getCurrentBlock().timestamp
98 return now >= self.startTime && now <= self.endTime
99 }
100
101 access(all) fun markClaimed(user: Address) {
102 assert(self.eligibleUsers[user] == true, message: "User not eligible for this drop")
103 assert(self.claimedUsers[user] == nil, message: "User already claimed")
104 assert(self.isActive(), message: "Drop window has expired")
105
106 self.claimedUsers[user] = getCurrentBlock().timestamp
107 }
108
109 access(all) fun getTimeRemaining(): UFix64 {
110 let now = getCurrentBlock().timestamp
111 if now >= self.endTime {
112 return 0.0
113 }
114 return self.endTime - now
115 }
116 }
117
118 /// Chapter 5 Status - Tracks slacker + overachiever completion
119 access(all) struct Chapter5Status {
120 access(all) let userAddress: Address
121 access(all) var slackerComplete: Bool
122 access(all) var overachieverComplete: Bool
123 access(all) var nftAirdropped: Bool
124 access(all) var nftID: UInt64?
125 access(all) var slackerTimestamp: UFix64
126 access(all) var overachieverTimestamp: UFix64
127 access(all) var completionTimestamp: UFix64
128
129 init(userAddress: Address) {
130 self.userAddress = userAddress
131 self.slackerComplete = false
132 self.overachieverComplete = false
133 self.nftAirdropped = false
134 self.nftID = nil
135 self.slackerTimestamp = 0.0
136 self.overachieverTimestamp = 0.0
137 self.completionTimestamp = 0.0
138 }
139
140 access(all) fun markSlackerComplete() {
141 self.slackerComplete = true
142 self.slackerTimestamp = getCurrentBlock().timestamp
143 self.checkFullCompletion()
144 }
145
146 access(all) fun markOverachieverComplete() {
147 self.overachieverComplete = true
148 self.overachieverTimestamp = getCurrentBlock().timestamp
149 self.checkFullCompletion()
150 }
151
152 access(all) fun checkFullCompletion() {
153 if self.slackerComplete && self.overachieverComplete && self.completionTimestamp == 0.0 {
154 self.completionTimestamp = getCurrentBlock().timestamp
155 }
156 }
157
158 access(all) fun isFullyComplete(): Bool {
159 return self.slackerComplete && self.overachieverComplete
160 }
161
162 access(all) fun markNFTAirdropped(nftID: UInt64) {
163 self.nftAirdropped = true
164 self.nftID = nftID
165 }
166 }
167
168 // ========================================
169 // USER PROFILE
170 // ========================================
171
172 /// UserProfile - Stores user's timezone for Paradise Motel day/night personalization
173 /// Created during first GumDrop claim (combo transaction)
174 access(all) resource UserProfile {
175 access(all) var username: String
176 access(all) var timezone: Int // UTC offset in hours (e.g., -7 for PDT, -5 for EST)
177
178 init(username: String, timezone: Int) {
179 self.username = username
180 self.timezone = timezone
181 }
182 }
183
184 // ========================================
185 // CHAPTER 5 NFT
186 // ========================================
187
188 access(all) resource Chapter5NFT: NonFungibleToken.NFT {
189 access(all) let id: UInt64
190 access(all) let achievementType: String
191 access(all) let recipient: Address
192 access(all) let mintedAt: UFix64
193 access(all) let serialNumber: UInt64 // Mint order (1st, 2nd, 3rd person to complete)
194 access(all) var metadata: {String: String} // Changed to 'var' so you can update it later
195
196 init(id: UInt64, recipient: Address, serialNumber: UInt64) {
197 self.id = id
198 self.achievementType = "SLACKER_AND_OVERACHIEVER"
199 self.recipient = recipient
200 self.mintedAt = getCurrentBlock().timestamp
201 self.serialNumber = serialNumber
202
203 self.metadata = {
204 "name": "Paradise Motel",
205 "description": "Awarded for completing both Slacker and Overachiever objectives in Chapter 5 of Flunks: Semester Zero",
206 "achievement": "SLACKER_AND_OVERACHIEVER",
207 "chapter": "5",
208 "collection": "Flunks: Semester Zero",
209 "serialNumber": serialNumber.toString(),
210 "revealed": "false",
211 "image": "https://storage.googleapis.com/flunks_public/images/1.png"
212 }
213 }
214
215 // Admin can update metadata for the reveal
216 access(contract) fun reveal(newMetadata: {String: String}) {
217 self.metadata = newMetadata
218 }
219
220 access(all) view fun getViews(): [Type] {
221 return [
222 Type<MetadataViews.Display>(),
223 Type<MetadataViews.NFTCollectionData>(),
224 Type<MetadataViews.NFTCollectionDisplay>(),
225 Type<MetadataViews.Royalties>(),
226 Type<MetadataViews.ExternalURL>(),
227 Type<MetadataViews.Serial>()
228 ]
229 }
230
231 access(all) fun resolveView(_ view: Type): AnyStruct? {
232 switch view {
233 case Type<MetadataViews.Display>():
234 return MetadataViews.Display(
235 name: self.metadata["name"]!,
236 description: self.metadata["description"]!,
237 thumbnail: MetadataViews.HTTPFile(url: self.metadata["image"]!)
238 )
239
240 case Type<MetadataViews.NFTCollectionData>():
241 return SemesterZero.resolveContractView(resourceType: nil, viewType: Type<MetadataViews.NFTCollectionData>())
242
243 case Type<MetadataViews.NFTCollectionDisplay>():
244 return SemesterZero.resolveContractView(resourceType: nil, viewType: Type<MetadataViews.NFTCollectionDisplay>())
245
246 case Type<MetadataViews.Royalties>():
247 let royaltyCap = SemesterZero.getRoyaltyReceiverCapability()
248 if !royaltyCap.check() {
249 return MetadataViews.Royalties([])
250 }
251 return MetadataViews.Royalties([
252 MetadataViews.Royalty(
253 receiver: royaltyCap,
254 cut: 0.10,
255 description: "Flunks: Semester Zero creator royalty"
256 )
257 ])
258
259 case Type<MetadataViews.ExternalURL>():
260 return MetadataViews.ExternalURL("https://flunks.flow")
261
262 case Type<MetadataViews.Serial>():
263 return MetadataViews.Serial(self.serialNumber)
264 }
265 return nil
266 }
267
268 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
269 return <-SemesterZero.createEmptyCollection(nftType: Type<@SemesterZero.Chapter5NFT>())
270 }
271 }
272
273 // ========================================
274 // CHAPTER 5 COLLECTION
275 // ========================================
276
277 access(all) resource Chapter5Collection: NonFungibleToken.Collection, ViewResolver.ResolverCollection {
278 access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
279
280 init() {
281 self.ownedNFTs <- {}
282 }
283
284 access(all) view fun getLength(): Int {
285 return self.ownedNFTs.length
286 }
287
288 access(all) view fun getIDs(): [UInt64] {
289 return self.ownedNFTs.keys
290 }
291
292 access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
293 return &self.ownedNFTs[id]
294 }
295
296 access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
297 let token <- self.ownedNFTs.remove(key: withdrawID)
298 ?? panic("NFT not found in collection")
299 let nft <- token as! @SemesterZero.Chapter5NFT
300 emit Withdraw(id: nft.id, from: self.owner?.address)
301 return <-nft
302 }
303
304 access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
305 let nft <- token as! @SemesterZero.Chapter5NFT
306 let id = nft.id
307 let oldToken <- self.ownedNFTs[id] <- nft
308 destroy oldToken
309 emit Deposit(id: id, to: self.owner?.address)
310 }
311
312 access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
313 return {Type<@SemesterZero.Chapter5NFT>(): true}
314 }
315
316 access(all) view fun isSupportedNFTType(type: Type): Bool {
317 return type == Type<@SemesterZero.Chapter5NFT>()
318 }
319
320 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
321 return <-SemesterZero.createEmptyCollection(nftType: Type<@SemesterZero.Chapter5NFT>())
322 }
323
324 // Borrow specific Chapter5NFT (needed for reveal function)
325 access(all) view fun borrowChapter5NFT(id: UInt64): &Chapter5NFT? {
326 if self.ownedNFTs[id] != nil {
327 let ref = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}?
328 return ref as! &Chapter5NFT?
329 }
330 return nil
331 }
332
333 // MetadataViews.ResolverCollection - Required for Token List
334 access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? {
335 if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? {
336 return nft as &{ViewResolver.Resolver}
337 }
338 return nil
339 }
340 }
341
342 // ========================================
343 // ADMIN RESOURCE
344 // ========================================
345
346 access(all) resource Admin {
347
348 // === GUM DROP MANAGEMENT ===
349
350 /// Create a new GumDrop with 72-hour claim window (called by Flow Actions)
351 access(all) fun createGumDrop(
352 dropId: String,
353 eligibleAddresses: [Address],
354 amount: UFix64,
355 durationSeconds: UFix64
356 ) {
357 pre {
358 SemesterZero.activeGumDrop == nil: "Active GumDrop already exists. Close it first."
359 eligibleAddresses.length > 0: "Must have at least one eligible user"
360 amount > 0.0: "Amount must be greater than 0"
361 durationSeconds > 0.0: "Duration must be greater than 0"
362 }
363
364 let drop = GumDrop(
365 dropId: dropId,
366 amount: amount,
367 eligibleUsers: eligibleAddresses,
368 durationSeconds: durationSeconds
369 )
370
371 SemesterZero.activeGumDrop = drop
372 SemesterZero.totalGumDrops = SemesterZero.totalGumDrops + 1
373
374 emit GumDropCreated(
375 dropId: dropId,
376 eligibleCount: eligibleAddresses.length,
377 amount: amount,
378 startTime: drop.startTime,
379 endTime: drop.endTime
380 )
381 }
382
383 /// Mark user as claimed (called after Supabase GUM is added)
384 access(all) fun markGumClaimed(user: Address) {
385 pre {
386 SemesterZero.activeGumDrop != nil: "No active GumDrop"
387 }
388
389 if let drop = SemesterZero.activeGumDrop {
390 drop.markClaimed(user: user)
391
392 emit GumDropClaimed(
393 user: user,
394 dropId: drop.dropId,
395 amount: drop.amount,
396 timestamp: getCurrentBlock().timestamp
397 )
398 }
399 }
400
401 /// Close the active GumDrop
402 access(all) fun closeGumDrop() {
403 pre {
404 SemesterZero.activeGumDrop != nil: "No active GumDrop to close"
405 }
406
407 if let drop = SemesterZero.activeGumDrop {
408 emit GumDropClosed(
409 dropId: drop.dropId,
410 totalClaimed: drop.claimedUsers.length,
411 totalEligible: drop.eligibleUsers.length
412 )
413 }
414
415 SemesterZero.activeGumDrop = nil
416 }
417
418 // === CHAPTER 5 COMPLETION MANAGEMENT ===
419
420 /// Register slacker completion
421 access(all) fun registerSlackerCompletion(userAddress: Address) {
422 if SemesterZero.chapter5Completions[userAddress] == nil {
423 SemesterZero.chapter5Completions[userAddress] = Chapter5Status(userAddress: userAddress)
424 }
425
426 SemesterZero.chapter5Completions[userAddress]?.markSlackerComplete()
427
428 emit Chapter5SlackerCompleted(
429 userAddress: userAddress,
430 timestamp: getCurrentBlock().timestamp
431 )
432
433 // Check if both are complete
434 self.checkFullCompletion(userAddress: userAddress)
435 }
436
437 /// Register overachiever completion
438 access(all) fun registerOverachieverCompletion(userAddress: Address) {
439 if SemesterZero.chapter5Completions[userAddress] == nil {
440 SemesterZero.chapter5Completions[userAddress] = Chapter5Status(userAddress: userAddress)
441 }
442
443 SemesterZero.chapter5Completions[userAddress]?.markOverachieverComplete()
444
445 emit Chapter5OverachieverCompleted(
446 userAddress: userAddress,
447 timestamp: getCurrentBlock().timestamp
448 )
449
450 // Check if both are complete
451 self.checkFullCompletion(userAddress: userAddress)
452 }
453
454 /// Check if both achievements complete and emit event
455 access(all) fun checkFullCompletion(userAddress: Address) {
456 if let status = SemesterZero.chapter5Completions[userAddress] {
457 if status.isFullyComplete() && !status.nftAirdropped {
458 SemesterZero.totalChapter5Completions = SemesterZero.totalChapter5Completions + 1
459
460 emit Chapter5FullCompletion(
461 userAddress: userAddress,
462 timestamp: getCurrentBlock().timestamp
463 )
464 }
465 }
466 }
467
468 /// Airdrop Chapter 5 NFT to eligible user
469 access(all) fun airdropChapter5NFT(userAddress: Address) {
470 assert(SemesterZero.isEligibleForChapter5NFT(userAddress: userAddress), message: "User not eligible for Chapter 5 NFT")
471
472 // Get recipient's collection capability
473 let recipientCap = getAccount(userAddress)
474 .capabilities.get<&SemesterZero.Chapter5Collection>(SemesterZero.Chapter5CollectionPublicPath)
475
476 assert(recipientCap.check(), message: "Recipient does not have Chapter 5 collection set up")
477
478 let recipient = recipientCap.borrow()!
479
480 // Mint NFT
481 let nftID = SemesterZero.totalChapter5NFTs
482 let serialNumber = SemesterZero.totalChapter5NFTs + 1 // 1st, 2nd, 3rd, etc.
483 SemesterZero.totalChapter5NFTs = SemesterZero.totalChapter5NFTs + 1
484
485 let nft <- create Chapter5NFT(
486 id: nftID,
487 recipient: userAddress,
488 serialNumber: serialNumber
489 )
490
491 // Deposit to recipient
492 recipient.deposit(token: <-nft)
493 emit Minted(id: nftID, recipient: userAddress)
494
495 // Update completion status
496 if let completionStatus = SemesterZero.chapter5Completions[userAddress] {
497 completionStatus.markNFTAirdropped(nftID: nftID)
498 }
499
500 emit Chapter5NFTMinted(
501 nftID: nftID,
502 recipient: userAddress,
503 timestamp: getCurrentBlock().timestamp
504 )
505 }
506
507 /// Reveal a user's Chapter 5 NFT (update metadata)
508 access(all) fun revealChapter5NFT(userAddress: Address, newMetadata: {String: String}) {
509 // Get user's collection
510 let collectionRef = getAccount(userAddress)
511 .capabilities.get<&SemesterZero.Chapter5Collection>(SemesterZero.Chapter5CollectionPublicPath)
512 .borrow()
513 ?? panic("User does not have Chapter 5 collection")
514
515 // Get their NFT IDs
516 let nftIDs = collectionRef.getIDs()
517 assert(nftIDs.length > 0, message: "User has no Chapter 5 NFTs")
518
519 // Borrow the NFT and reveal it
520 let nftID = nftIDs[0]
521 let nftRef = collectionRef.borrowChapter5NFT(id: nftID)
522 ?? panic("Could not borrow NFT reference")
523
524 nftRef.reveal(newMetadata: newMetadata)
525 }
526
527 /// Burn (permanently destroy) an NFT from the signer's collection
528 /// The signer must be an admin and own the NFT
529 access(all) fun burnNFTFromCollection(collection: auth(NonFungibleToken.Withdraw) &Chapter5Collection, nftID: UInt64) {
530 // Verify the NFT exists
531 assert(collection.ownedNFTs[nftID] != nil, message: "NFT does not exist in collection")
532
533 // Withdraw and destroy
534 let nft <- collection.withdraw(withdrawID: nftID)
535 let ownerAddress = collection.owner!.address
536
537 emit Chapter5NFTBurned(
538 nftID: nftID,
539 owner: ownerAddress,
540 timestamp: getCurrentBlock().timestamp
541 )
542
543 destroy nft
544 }
545 }
546
547 // ========================================
548 // PUBLIC FUNCTIONS
549 // ========================================
550
551 /// Create user profile (timezone for Paradise Motel day/night)
552 access(all) fun createUserProfile(username: String, timezone: Int): @UserProfile {
553 return <- create UserProfile(username: username, timezone: timezone)
554 }
555
556 /// Create empty Chapter 5 collection
557 access(all) fun createEmptyChapter5Collection(): @Chapter5Collection {
558 return <- create Chapter5Collection()
559 }
560
561 access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
562 assert(nftType == Type<@SemesterZero.Chapter5NFT>(), message: "Unsupported NFT type")
563 return <- SemesterZero.createEmptyChapter5Collection()
564 }
565
566 access(all) fun getRoyaltyReceiverCapability(): Capability<&{FungibleToken.Receiver}> {
567 return getAccount(0xbfffec679fff3a94)
568 .capabilities
569 .get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
570 }
571
572 // ========================================
573 // QUERY FUNCTIONS
574 // ========================================
575
576 /// Check if user is eligible for GumDrop
577 access(all) fun isEligibleForGumDrop(user: Address): Bool {
578 if let drop = SemesterZero.activeGumDrop {
579 return drop.isEligible(user: user) && drop.isActive()
580 }
581 return false
582 }
583
584 /// Check if user has claimed GumDrop
585 access(all) fun hasClaimedGumDrop(user: Address): Bool {
586 if let drop = SemesterZero.activeGumDrop {
587 return drop.hasClaimed(user: user)
588 }
589 return false
590 }
591
592 /// Get active GumDrop info
593 access(all) fun getGumDropInfo(): {String: AnyStruct}? {
594 if let drop = SemesterZero.activeGumDrop {
595 return {
596 "dropId": drop.dropId,
597 "amount": drop.amount,
598 "startTime": drop.startTime,
599 "endTime": drop.endTime,
600 "isActive": drop.isActive(),
601 "timeRemaining": drop.getTimeRemaining(),
602 "totalEligible": drop.eligibleUsers.length,
603 "totalClaimed": drop.claimedUsers.length
604 }
605 }
606 return nil
607 }
608
609 /// Get Chapter 5 status for user
610 access(all) fun getChapter5Status(userAddress: Address): Chapter5Status? {
611 return SemesterZero.chapter5Completions[userAddress]
612 }
613
614 /// Check if user is eligible for Chapter 5 NFT airdrop
615 access(all) fun isEligibleForChapter5NFT(userAddress: Address): Bool {
616 if let status = SemesterZero.chapter5Completions[userAddress] {
617 return status.isFullyComplete() && !status.nftAirdropped
618 }
619 return false
620 }
621
622 /// Get contract stats
623 access(all) fun getStats(): {String: UInt64} {
624 return {
625 "totalGumDrops": SemesterZero.totalGumDrops,
626 "totalChapter5Completions": SemesterZero.totalChapter5Completions,
627 "totalChapter5NFTs": SemesterZero.totalChapter5NFTs
628 }
629 }
630
631 // ========================================
632 // VIEW RESOLVER (for Token List Registration)
633 // ========================================
634
635 /// Returns the types of supported views - called by tokenlist
636 access(all) view fun getContractViews(resourceType: Type?): [Type] {
637 return [
638 Type<MetadataViews.NFTCollectionData>(),
639 Type<MetadataViews.NFTCollectionDisplay>()
640 ]
641 }
642
643 /// Resolves a view for this contract - called by tokenlist
644 access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
645 switch viewType {
646 case Type<MetadataViews.NFTCollectionData>():
647 return MetadataViews.NFTCollectionData(
648 storagePath: self.Chapter5CollectionStoragePath,
649 publicPath: self.Chapter5CollectionPublicPath,
650 publicCollection: Type<&Chapter5Collection>(),
651 publicLinkedType: Type<&Chapter5Collection>(),
652 createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} {
653 return <-SemesterZero.createEmptyCollection(nftType: Type<@SemesterZero.Chapter5NFT>())
654 })
655 )
656 case Type<MetadataViews.NFTCollectionDisplay>():
657 let squareMedia = MetadataViews.Media(
658 file: MetadataViews.HTTPFile(
659 url: "https://storage.googleapis.com/flunks_public/images/semesterzero.png"
660 ),
661 mediaType: "image/png"
662 )
663 let bannerMedia = MetadataViews.Media(
664 file: MetadataViews.HTTPFile(
665 url: "https://storage.googleapis.com/flunks_public/images/banner.png"
666 ),
667 mediaType: "image/png"
668 )
669 return MetadataViews.NFTCollectionDisplay(
670 name: "Flunks: Semester Zero",
671 description: "Flunks: Semester Zero is a standalone collection that rewards users for exploring flunks.net and participating in events, challenges and completing objectives.",
672 externalURL: MetadataViews.ExternalURL("https://flunks.net"),
673 squareImage: squareMedia,
674 bannerImage: bannerMedia,
675 socials: {
676 "twitter": MetadataViews.ExternalURL("https://x.com/flunks_nft"),
677 "discord": MetadataViews.ExternalURL("https://discord.gg/flunks")
678 }
679 )
680 }
681 return nil
682 }
683
684 // ========================================
685 // INITIALIZATION
686 // ========================================
687
688 init() {
689 // Set storage paths
690 self.UserProfileStoragePath = /storage/SemesterZeroProfile
691 self.UserProfilePublicPath = /public/SemesterZeroProfile
692 self.Chapter5CollectionStoragePath = /storage/SemesterZeroChapter5Collection
693 self.Chapter5CollectionPublicPath = /public/SemesterZeroChapter5Collection
694 self.AdminStoragePath = /storage/SemesterZeroHackathonAdmin
695
696 // Initialize state
697 self.totalGumDrops = 0
698 self.totalChapter5Completions = 0
699 self.totalChapter5NFTs = 0
700 self.activeGumDrop = nil
701 self.chapter5Completions = {}
702
703 // Create admin resource (only if it doesn't exist)
704 if self.account.storage.borrow<&Admin>(from: self.AdminStoragePath) == nil {
705 let admin <- create Admin()
706 self.account.storage.save(<-admin, to: self.AdminStoragePath)
707 }
708
709 emit ContractInitialized()
710 }
711}
712
713