Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions motoko/nft-creator/frontend/index.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
<html lang='en'>
<head>
<meta charset='UTF-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title>NFT Collection Management Application</title>
</head>
<body>
<div id='root'></div>
<script type='module' src='/src/main.tsx'></script>
</body>

<head>
<meta charset='UTF-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title>NFT Collection Management Application</title>
</head>

<body>
<div id='root'></div>
<script type='module' src='/src/main.jsx'></script>
</body>

</html>
5 changes: 1 addition & 4 deletions motoko/nft-creator/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"scripts": {
"dev": "vite",
"prebuild": "npm i --include=dev && dfx generate",
"build": "tsc && vite build"
"build": "vite build"
},
"dependencies": {
"@dfinity/agent": "^3.0.0",
Expand All @@ -21,12 +21,9 @@
"tailwindcss": "^4.1.11"
},
"devDependencies": {
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^4.7.0",
"dotenv": "^17.2.1",
"sass": "^1.89.2",
"typescript": "^5.8.3",
"vite": "^7.0.6",
"vite-plugin-environment": "^1.1.3"
}
Expand Down
98 changes: 98 additions & 0 deletions motoko/nft-creator/frontend/src/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { useInternetIdentity } from "ic-use-internet-identity";
import { CollectionClaim } from "./components/CollectionClaim";
import { MintNFT } from "./components/MintNFT";
import { OwnedNFTs } from "./components/OwnedNFTs";
import { AuthButton } from "./components/AuthButton";
import { Heart } from "lucide-react";

function App() {
const { identity, isInitializing } = useInternetIdentity();

if (isInitializing) {
return (
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-purple-500"></div>
</div>
);
}

return (
<div className="min-h-screen bg-gray-900 text-white">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-8">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6 sm:mb-8">
<h1 className="text-2xl sm:text-3xl font-bold bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent text-center sm:text-left">
NFT Collection Manager
</h1>
<div className="flex justify-center sm:justify-end">
<AuthButton />
</div>
</div>

{identity ? (
<div className="space-y-6 sm:space-y-8">
{/* Collection Management */}
<div className="bg-gray-800 rounded-lg p-4 sm:p-6 border border-gray-700">
<h2 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4 text-purple-300">
Collection Management
</h2>
<CollectionClaim />
</div>

{/* Minting Section */}
<div className="bg-gray-800 rounded-lg p-4 sm:p-6 border border-gray-700">
<h2 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4 text-purple-300">
Mint NFT
</h2>
<MintNFT />
</div>

{/* Owned NFTs */}
<div className="bg-gray-800 rounded-lg p-4 sm:p-6 border border-gray-700">
<h2 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4 text-purple-300">
Your NFTs
</h2>
<OwnedNFTs />
</div>
</div>
) : (
<div className="text-center py-8 sm:py-16">
<div className="bg-gray-800 rounded-lg p-6 sm:p-8 max-w-sm sm:max-w-md mx-auto border border-gray-700">
<h2 className="text-xl sm:text-2xl font-semibold mb-3 sm:mb-4">
Welcome to NFT Collection Manager
</h2>
<p className="text-gray-400 mb-4 sm:mb-6 text-sm sm:text-base">
Please authenticate with Internet Identity to
manage your NFT collection.
</p>
<div className="flex justify-center">
<AuthButton />
</div>
</div>
</div>
)}

{/* Footer */}
<footer className="mt-12 sm:mt-16 text-center text-gray-500 text-xs sm:text-sm px-4">
<div className="flex flex-col sm:flex-row items-center justify-center gap-1">
<span>
© 2025. Built with{" "}
<Heart className="inline w-4 h-4 text-red-500" />{" "}
using
</span>
<a
href="https://caffeine.ai"
target="_blank"
rel="noopener noreferrer"
className="text-purple-400 hover:text-purple-300 transition-colors"
>
caffeine.ai
</a>
</div>
</footer>
</div>
</div>
);
}

export default App;
85 changes: 0 additions & 85 deletions motoko/nft-creator/frontend/src/App.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { useCollectionOwner, useMintNFT } from "../hooks/useQueries";
import { useInternetIdentity } from "ic-use-internet-identity";
import { Principal } from "@dfinity/principal";
import { Sparkles } from "lucide-react";
import { useToast } from "../contexts/ToastContext";

export function MintNFT() {
const { identity } = useInternetIdentity();
const { data: collectionOwner, isLoading: isLoadingOwner } =
useCollectionOwner();
const { mutate: mintNFT, isPending: isMinting } = useMintNFT();
const { addError } = useToast();

const [recipient, setRecipient] = useState("");

Expand Down Expand Up @@ -56,6 +58,7 @@ export function MintNFT() {
setRecipient("");
} catch (error) {
console.error("Invalid principal:", error);
addError("Invalid principal: " + (error?.message || error));
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,16 @@ import { useState } from "react";
import { useTransferNFT } from "../hooks/useQueries";
import { Principal } from "@dfinity/principal";
import { Send, Image as ImageIcon } from "lucide-react";
import type { Metadata } from "../types";
import { useToast } from "../contexts/ToastContext";

interface NFTData {
tokenId: bigint;
metadata: Metadata;
}

interface NFTCardProps {
nft: NFTData;
}

export function NFTCard({ nft }: NFTCardProps) {
export function NFTCard({ nft }) {
const [recipient, setRecipient] = useState("");
const [imageError, setImageError] = useState(false);
const { mutate: transferNFT, isPending: isTransferring } = useTransferNFT();
const { addError } = useToast();

// Helper function to get metadata value by key
const getMetadataValue = (key: string): string => {
const getMetadataValue = (key) => {
const entry = nft.metadata[0]?.find(([k]) => k === key);
if (entry && entry[1] && "Text" in entry[1]) {
return entry[1].Text;
Expand All @@ -39,6 +31,7 @@ export function NFTCard({ nft }: NFTCardProps) {
setRecipient("");
} catch (error) {
console.error("Invalid principal:", error);
addError("Invalid principal: " + (error?.message || error));
}
};

Expand Down
97 changes: 97 additions & 0 deletions motoko/nft-creator/frontend/src/contexts/ToastContext.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { createContext, useContext, useState, useCallback } from "react";
import { X } from "lucide-react";

const ToastContext = createContext(undefined);

export function useToast() {
const context = useContext(ToastContext);
if (!context) {
throw new Error("useToast must be used within a ToastProvider");
}
return context;
}

export function ToastProvider({ children }) {
const [toasts, setToasts] = useState([]);

const addToast = useCallback((message, type) => {
const id = Date.now().toString();
const newToast = { id, message, type };

setToasts((prev) => [newToast, ...prev]); // Latest first

// Auto remove after 5 seconds
setTimeout(() => {
removeToast(id);
}, 5000);
}, []);

const removeToast = useCallback((id) => {
setToasts((prev) => prev.filter((toast) => toast.id !== id));
}, []);

const addError = useCallback(
(message) => addToast(message, "error"),
[addToast]
);
const addSuccess = useCallback(
(message) => addToast(message, "success"),
[addToast]
);
const addInfo = useCallback(
(message) => addToast(message, "info"),
[addToast]
);

return (
<ToastContext.Provider
value={{
toasts,
addToast,
removeToast,
addError,
addSuccess,
addInfo,
}}
>
{children}
<ToastContainer toasts={toasts} onRemove={removeToast} />
</ToastContext.Provider>
);
}

function ToastContainer({ toasts, onRemove }) {
return (
<div className="fixed bottom-4 left-4 z-50 space-y-2">
{toasts.map((toast, index) => (
<div
key={toast.id}
className={`
flex items-center gap-3 p-4 rounded-lg shadow-lg max-w-md
transform transition-all duration-300 ease-in-out
${index > 0 ? "opacity-80" : "opacity-100"}
${toast.type === "error" ? "bg-red-600 text-white" : ""}
${toast.type === "success" ? "bg-green-600 text-white" : ""}
${toast.type === "info" ? "bg-blue-600 text-white" : ""}
animate-slide-in
`}
style={{
transform: `translateY(${index * -8}px)`,
zIndex: 50 - index,
}}
>
<div className="flex-1 text-sm font-medium">
{toast.message}
</div>
<button
onClick={() => onRemove(toast.id)}
className="flex-shrink-0 p-1 hover:bg-black/20 rounded transition-colors"
aria-label="Close notification"
>
<X className="w-4 h-4" />
</button>
</div>
))}
</div>
);
}
Loading