Smart Contract

SemesterZero

A.807c3d470888cc48.SemesterZero

Valid From

132,123,619

Deployed

1w ago
Feb 19, 2026, 10:24:09 AM UTC

Dependents

21 imports
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