├── .nvmrc ├── .env.icprod ├── .env.iclocal ├── .env.production ├── .env.development ├── .gitattributes ├── public ├── logo.png ├── .ic-assets.json5 └── demo-screenshot.png ├── .prettierrc.json ├── backend ├── service │ ├── Hello.mo │ └── Image.mo ├── model │ └── ImageType.mo └── helper │ └── ImageStoreHelper.mo ├── pages ├── _app.js └── index.js ├── ui ├── declarations │ ├── hello │ │ ├── hello.did.js │ │ └── index.js │ └── image │ │ ├── image.did.js │ │ └── index.js ├── styles │ ├── Home.module.css │ └── global.css ├── service │ ├── image-service.js │ └── actor-locator.js ├── hooks │ └── useImageObject.js ├── utils │ └── image.js └── components │ ├── GreetingSection.js │ └── ImageSection.js ├── .gitignore ├── next.config.js ├── dfx.json ├── dfx.webpack.config.js ├── LICENSE ├── .eslintrc ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.12.0 -------------------------------------------------------------------------------- /.env.icprod: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_IC_HOST=https://ic0.app 2 | DFX_NETWORK=ic -------------------------------------------------------------------------------- /.env.iclocal: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_IC_HOST=http://localhost:8000 2 | DFX_NETWORK=local -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_IC_HOST=http://localhost:8000 2 | DFX_NETWORK=local -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_IC_HOST=http://localhost:8000 2 | DFX_NETWORK=local -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dappblock/nextjs-ic-starter/HEAD/public/logo.png -------------------------------------------------------------------------------- /public/.ic-assets.json5: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | match: "**/*", 4 | security_policy: "standard" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /public/demo-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dappblock/nextjs-ic-starter/HEAD/public/demo-screenshot.png -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "trailingComma": "none", 4 | "semi": false, 5 | "arrowParens": "avoid" 6 | } 7 | -------------------------------------------------------------------------------- /backend/service/Hello.mo: -------------------------------------------------------------------------------- 1 | actor { 2 | public query func greet(name : Text) : async Text { 3 | "Hello, " # name # "!"; 4 | }; 5 | }; 6 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import "../ui/styles/global.css" 2 | 3 | export default function App({ Component, pageProps }) { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /ui/declarations/hello/hello.did.js: -------------------------------------------------------------------------------- 1 | export const idlFactory = ({ IDL }) => { 2 | return IDL.Service({ 'greet' : IDL.Func([IDL.Text], [IDL.Text], ['query']) }); 3 | }; 4 | export const init = ({ IDL }) => { return []; }; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Various IDEs and Editors 2 | .vscode/ 3 | .idea/ 4 | **/*~ 5 | 6 | # Mac OSX temporary files 7 | .DS_Store 8 | **/.DS_Store 9 | 10 | # dfx temporary files 11 | .dfx/ 12 | canister_ids.json 13 | 14 | # frontend code 15 | node_modules/ 16 | out/ 17 | 18 | # Next.js 19 | .next/ 20 | 21 | -------------------------------------------------------------------------------- /ui/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0 0.5rem; 4 | } 5 | 6 | .main { 7 | padding: 5rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | } 13 | 14 | .title { 15 | margin: 0; 16 | line-height: 1.15; 17 | font-size: 2rem; 18 | text-align: center; 19 | } 20 | 21 | .logo { 22 | width: 140px; 23 | } -------------------------------------------------------------------------------- /ui/declarations/image/image.did.js: -------------------------------------------------------------------------------- 1 | export const idlFactory = ({ IDL }) => { 2 | const ImageObject = IDL.Vec(IDL.Nat8); 3 | const ImageId = IDL.Text; 4 | return IDL.Service({ 5 | 'create' : IDL.Func([ImageObject], [ImageId], []), 6 | 'delete' : IDL.Func([ImageId], [], []), 7 | 'getImageById' : IDL.Func([ImageId], [IDL.Opt(ImageObject)], ['query']), 8 | }); 9 | }; 10 | export const init = ({ IDL }) => { return []; }; 11 | -------------------------------------------------------------------------------- /ui/service/image-service.js: -------------------------------------------------------------------------------- 1 | import { arrayBufferToImgSrc } from "../utils/image" 2 | import { makeImageActor } from "./actor-locator" 3 | 4 | export const loadImage = async id => { 5 | const actorService = await makeImageActor() 6 | const result = await actorService.getImageById(id) 7 | if (result.length == 0) { 8 | return null 9 | } 10 | 11 | const imageArray = result[0] 12 | const imageSource = arrayBufferToImgSrc(imageArray) 13 | return imageSource 14 | } 15 | -------------------------------------------------------------------------------- /backend/model/ImageType.mo: -------------------------------------------------------------------------------- 1 | import Time "mo:base/Time"; 2 | import Int "mo:base/Int"; 3 | import Trie "mo:base/Trie"; 4 | import Text "mo:base/Text"; 5 | import Iter "mo:base/Iter"; 6 | import List "mo:base/List"; 7 | import Buffer "mo:base/Buffer"; 8 | 9 | module ImageType { 10 | 11 | public type ImageObject = [Nat8]; 12 | public type ImageId = Text; 13 | 14 | public func generateNewRemoteObjectId() : ImageId { 15 | return Int.toText(Time.now()); 16 | }; 17 | 18 | public func imageIdKey (x: ImageId) : Trie.Key{ 19 | { key=x; hash = Text.hash(x) } 20 | }; 21 | 22 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const DFXWebPackConfig = require("./dfx.webpack.config") 2 | DFXWebPackConfig.initCanisterIds() 3 | 4 | const webpack = require("webpack") 5 | 6 | // Make DFX_NETWORK available to Web Browser with default "local" if DFX_NETWORK is undefined 7 | const EnvPlugin = new webpack.EnvironmentPlugin({ 8 | DFX_NETWORK: "local" 9 | }) 10 | 11 | module.exports = { 12 | webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { 13 | // Plugin 14 | config.plugins.push(EnvPlugin) 15 | 16 | // Important: return the modified config 17 | return config 18 | }, 19 | output: "export" 20 | } 21 | -------------------------------------------------------------------------------- /ui/service/actor-locator.js: -------------------------------------------------------------------------------- 1 | import { 2 | createActor as createHelloActor, 3 | canisterId as helloCanisterId 4 | } from "../declarations/hello" 5 | 6 | import { 7 | createActor as createImageActor, 8 | canisterId as imageCanisterId 9 | } from "../declarations/image" 10 | 11 | export const makeActor = (canisterId, createActor) => { 12 | return createActor(canisterId, { 13 | agentOptions: { 14 | host: process.env.NEXT_PUBLIC_IC_HOST 15 | } 16 | }) 17 | } 18 | 19 | export function makeHelloActor() { 20 | return makeActor(helloCanisterId, createHelloActor) 21 | } 22 | 23 | export function makeImageActor() { 24 | return makeActor(imageCanisterId, createImageActor) 25 | } 26 | -------------------------------------------------------------------------------- /dfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "canisters": { 3 | "hello": { 4 | "main": "backend/service/Hello.mo", 5 | "type": "motoko", 6 | "declarations": { 7 | "node_compatibility": true 8 | } 9 | }, 10 | "image": { 11 | "main": "backend/service/Image.mo", 12 | "type": "motoko", 13 | "declarations": { 14 | "node_compatibility": true 15 | } 16 | }, 17 | "hello_assets": { 18 | "dependencies": ["hello"], 19 | "frontend": { 20 | "entrypoint": "out/index.html" 21 | }, 22 | "source": ["out"], 23 | "type": "assets" 24 | } 25 | }, 26 | "defaults": { 27 | "build": { 28 | "args": "", 29 | "packtool": "" 30 | } 31 | }, 32 | "output_env_file": ".env", 33 | "version": 1, 34 | "dfx": "0.25.0" 35 | } 36 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | // Next, React 3 | import Head from "next/head" 4 | 5 | import styles from "../ui/styles/Home.module.css" 6 | 7 | import { GreetingSection } from "../ui/components/GreetingSection" 8 | import { ImageSection } from "../ui/components/ImageSection" 9 | 10 | function HomePage() { 11 | return ( 12 |
13 | 14 | Internet Computer 15 | 16 |
17 |

18 | Welcome to Next.js Internet Computer Starter Template! 19 |

20 | 21 | DFINITY logo 22 | 23 | 24 | 25 |
26 |
27 | ) 28 | } 29 | 30 | export default HomePage 31 | -------------------------------------------------------------------------------- /ui/styles/global.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | line-height: 1.6; 8 | font-size: 18px; 9 | } 10 | 11 | * { 12 | box-sizing: border-box; 13 | } 14 | 15 | a { 16 | color: #0070f3; 17 | text-decoration: none; 18 | } 19 | 20 | a:hover { 21 | text-decoration: underline; 22 | } 23 | 24 | img { 25 | max-width: 100%; 26 | display: block; 27 | } 28 | 29 | button { 30 | width: 150px; 31 | height: 40px; 32 | font-size: 1rem; 33 | margin-left: 6px; 34 | margin-right: 6px; 35 | } 36 | 37 | section { 38 | padding: 6px; 39 | width: 600px 40 | } 41 | 42 | input { 43 | padding: 4px; 44 | margin: 0px 12px; 45 | width: 200px; 46 | height: 40px; 47 | font-size: 1rem; 48 | } 49 | 50 | label { 51 | font-size: 1.1rem; 52 | } -------------------------------------------------------------------------------- /ui/hooks/useImageObject.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react" 2 | import { loadImage } from "../service/image-service" 3 | 4 | export const useImageObject = imageId => { 5 | const [imgSrc, setImgSrc] = useState("") 6 | const [imgId, setImgId] = useState("") 7 | 8 | useEffect(() => { 9 | async function fetchImage() { 10 | if (imageId && imageId != "") { 11 | if (imageId == imgId) { 12 | // return existing src 13 | return imgSrc 14 | } 15 | 16 | const imageSource = await loadImage(imageId) 17 | 18 | // Make sure to revoke the data uris to avoid memory leaks 19 | if (imgSrc && imgSrc != "") URL.revokeObjectURL(imgSrc) 20 | 21 | setImgSrc(imageSource) 22 | setImgId(imageId) 23 | } else { 24 | setImgSrc(null) 25 | } 26 | } 27 | 28 | fetchImage() 29 | 30 | // eslint-disable-next-line react-hooks/exhaustive-deps 31 | }, [imageId]) 32 | 33 | return imgSrc 34 | } 35 | -------------------------------------------------------------------------------- /backend/service/Image.mo: -------------------------------------------------------------------------------- 1 | import Nat "mo:base/Nat"; 2 | import Nat32 "mo:base/Nat32"; 3 | import Trie "mo:base/Trie"; 4 | import Text "mo:base/Text"; 5 | 6 | import ImageType "../model/ImageType"; 7 | import ImageStoreHelper "../helper/ImageStoreHelper"; 8 | 9 | actor ImageBucket { 10 | 11 | type ImageId = ImageType.ImageId; 12 | type ImageObject = ImageType.ImageObject; 13 | 14 | stable var imageObjectStore : Trie.Trie = Trie.empty(); 15 | 16 | // atomic, no await allowed 17 | public func create(image: ImageObject) : async ImageId { 18 | let imageId = ImageType.generateNewRemoteObjectId(); 19 | imageObjectStore := ImageStoreHelper.addNewImage(imageObjectStore, image, imageId); 20 | 21 | return imageId; 22 | }; 23 | 24 | public query func getImageById(id: ImageId) : async ?ImageObject { 25 | return ImageStoreHelper.getImageById(id, imageObjectStore); 26 | }; 27 | 28 | // atomic, no await allowed 29 | public func delete(id: ImageId) : async () { 30 | imageObjectStore := ImageStoreHelper.removeImage(imageObjectStore, id); 31 | }; 32 | 33 | }; -------------------------------------------------------------------------------- /dfx.webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | 3 | let localCanisters, prodCanisters, canisters 4 | 5 | function initCanisterIds() { 6 | try { 7 | localCanisters = require(path.resolve(".dfx", "local", "canister_ids.json")) 8 | } catch (error) { 9 | console.log("No local canister_ids.json found. Continuing production") 10 | } 11 | try { 12 | prodCanisters = require(path.resolve("canister_ids.json")) 13 | } catch (error) { 14 | console.log("No production canister_ids.json found. Continuing with local") 15 | } 16 | 17 | const network = 18 | process.env.DFX_NETWORK || 19 | (process.env.NODE_ENV === "production" ? "ic" : "local") 20 | 21 | console.info(`initCanisterIds: network=${network}`) 22 | console.info(`initCanisterIds: DFX_NETWORK=${process.env.DFX_NETWORK}`) 23 | 24 | canisters = network === "local" ? localCanisters : prodCanisters 25 | 26 | for (const canister in canisters) { 27 | process.env[`NEXT_PUBLIC_${canister.toUpperCase()}_CANISTER_ID`] = 28 | canisters[canister][network] 29 | } 30 | } 31 | 32 | module.exports = { 33 | initCanisterIds: initCanisterIds 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Henry Chan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/helper/ImageStoreHelper.mo: -------------------------------------------------------------------------------- 1 | import Trie "mo:base/Trie"; 2 | import Text "mo:base/Text"; 3 | 4 | import ImageType "../model/ImageType"; 5 | 6 | module ImageStoreHelper { 7 | 8 | type ImageStore = Trie.Trie; 9 | type ImageObject = ImageType.ImageObject; 10 | type ImageId = ImageType.ImageId; 11 | 12 | public func addNewImage(imageObjectStore: ImageStore, image: ImageObject, imageId: ImageId) : ImageStore { 13 | let newStore = Trie.put( 14 | imageObjectStore, 15 | ImageType.imageIdKey(imageId), 16 | Text.equal, 17 | image 18 | ).0; 19 | 20 | return newStore; 21 | }; 22 | 23 | public func removeImage(imageObjectStore: ImageStore, imageId: ImageId) : ImageStore { 24 | let newStore = Trie.remove( 25 | imageObjectStore, 26 | ImageType.imageIdKey(imageId), 27 | Text.equal, 28 | ).0; 29 | 30 | return newStore; 31 | }; 32 | 33 | public func getImageById(id: ImageId, imageObjectStore: Trie.Trie) : ?ImageObject { 34 | return Trie.find(imageObjectStore, ImageType.imageIdKey(id), Text.equal); 35 | }; 36 | 37 | } -------------------------------------------------------------------------------- /ui/utils/image.js: -------------------------------------------------------------------------------- 1 | import Compressor from "compressorjs" 2 | 3 | export function arrayBufferToImgSrc(arrayBuffer, imgType = "jpeg") { 4 | const byteArray = new Uint8Array(arrayBuffer) 5 | const picBlob = new Blob([byteArray], { type: `image/${imgType}` }) 6 | const picSrc = URL.createObjectURL(picBlob) 7 | return picSrc 8 | } 9 | 10 | async function readFileToArrayBuffer(file) { 11 | return new Promise((resolve, reject) => { 12 | let reader = new FileReader() 13 | 14 | reader.onload = () => { 15 | resolve(reader.result) 16 | } 17 | 18 | reader.onerror = reject 19 | reader.readAsArrayBuffer(file) 20 | }) 21 | } 22 | 23 | export async function fileToCanisterBinaryStoreFormat(file) { 24 | const arrayBuffer = await readFileToArrayBuffer(file) 25 | return Array.from(new Uint8Array(arrayBuffer)) 26 | } 27 | 28 | const DefauttMaxWidth = 768 29 | 30 | export const resizeImage = async (file, maxWidth) => { 31 | return new Promise(resolve => { 32 | new Compressor(file, { 33 | quality: 0.8, 34 | maxWidth: maxWidth || DefauttMaxWidth, 35 | mimeType: "image/jpeg", 36 | success(result) { 37 | resolve(result) 38 | }, 39 | error(err) { 40 | resolve(err) 41 | } 42 | }) 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /ui/components/GreetingSection.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | 3 | // Dfinity 4 | import { makeHelloActor } from "../service/actor-locator" 5 | 6 | export const GreetingSection = () => { 7 | const [name, setName] = useState("") 8 | const [loading, setLoading] = useState("") 9 | const [greetingMessage, setGreetingMessage] = useState("") 10 | 11 | function onChangeName(e) { 12 | const newName = e.target.value 13 | setName(newName) 14 | } 15 | 16 | async function sayGreeting() { 17 | setGreetingMessage("") 18 | setLoading("Loading...") 19 | 20 | const helloActor = makeHelloActor() 21 | const greeting = await helloActor.greet(name) 22 | 23 | setLoading("") 24 | setGreetingMessage(greeting) 25 | } 26 | 27 | return ( 28 |
29 |
30 |

Greeting

31 | 32 | 39 | 40 |
41 |
42 | 43 | {loading} 44 | {greetingMessage} 45 |
46 |
47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next", 3 | "plugins": ["react"], 4 | "globals": { 5 | "console": "readonly", 6 | "module": "readonly", 7 | "process": "readonly", 8 | "BigInt": "readonly", 9 | "require": "readonly", 10 | "Blob": "readonly", 11 | "URL": "readonly", 12 | "page": "readonly", 13 | "browser": "readonly", 14 | "context": "readonly", 15 | "Promise": "readonly", 16 | "setTimeout": "readonly", 17 | "localStorage": "readonly", 18 | "Uint8Array": "readonly", 19 | "FileReader": "readonly", 20 | "Set": "readonly", 21 | "Math": "readonly", 22 | "window": "readonly", 23 | "Map": "readonly" 24 | }, 25 | "rules": { 26 | "eqeqeq": 0, 27 | "no-await-in-loop": 2, 28 | "no-shadow": 0, 29 | "no-use-before-define": 0, 30 | "no-unused-vars": 2, 31 | "no-undef": 2, 32 | "no-throw-literal": 2, 33 | "react/forbid-prop-types": 0, 34 | "react/jsx-curly-newline": 0, 35 | "react/prefer-stateless-function": 0, 36 | "react/state-in-constructor": 0, 37 | "react/sort-comp": 0, 38 | "default-case": 0, 39 | "no-param-reassign": 0, 40 | "class-methods-use-this": 0, 41 | "no-restricted-syntax": 0, 42 | "arrow-body-style": 0, 43 | "prefer-const": 1 44 | }, 45 | "ignorePatterns": [ 46 | "ui/declarations/**/*.js", 47 | "ui/declarations/**/*.ts", 48 | "src/declarations/**" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /ui/declarations/hello/index.js: -------------------------------------------------------------------------------- 1 | import { Actor, HttpAgent } from "@dfinity/agent" 2 | 3 | // Imports and re-exports candid interface 4 | import { idlFactory } from "./hello.did.js" 5 | export { idlFactory } from "./hello.did.js" 6 | 7 | /* CANISTER_ID is replaced by webpack based on node environment 8 | * Note: canister environment variable will be standardized as 9 | * process.env.CANISTER_ID_ 10 | * beginning in dfx 0.15.0 11 | */ 12 | export const canisterId = 13 | process.env.CANISTER_ID_HELLO || process.env.NEXT_PUBLIC_HELLO_CANISTER_ID 14 | 15 | export const createActor = (canisterId, options = {}) => { 16 | const agent = options.agent || new HttpAgent({ ...options.agentOptions }) 17 | 18 | if (options.agent && options.agentOptions) { 19 | console.warn( 20 | "Detected both agent and agentOptions passed to createActor. Ignoring agentOptions and proceeding with the provided agent." 21 | ) 22 | } 23 | 24 | // Fetch root key for certificate validation during development 25 | if (process.env.DFX_NETWORK !== "ic") { 26 | agent.fetchRootKey().catch(err => { 27 | console.warn( 28 | "Unable to fetch root key. Check to ensure that your local replica is running" 29 | ) 30 | console.error(err) 31 | }) 32 | } 33 | 34 | // Creates an actor with using the candid interface and the HttpAgent 35 | return Actor.createActor(idlFactory, { 36 | agent, 37 | canisterId, 38 | ...options.actorOptions 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /ui/declarations/image/index.js: -------------------------------------------------------------------------------- 1 | import { Actor, HttpAgent } from "@dfinity/agent" 2 | 3 | // Imports and re-exports candid interface 4 | import { idlFactory } from "./image.did.js" 5 | export { idlFactory } from "./image.did.js" 6 | 7 | /* CANISTER_ID is replaced by webpack based on node environment 8 | * Note: canister environment variable will be standardized as 9 | * process.env.CANISTER_ID_ 10 | * beginning in dfx 0.15.0 11 | */ 12 | export const canisterId = 13 | process.env.CANISTER_ID_IMAGE || process.env.NEXT_PUBLIC_IMAGE_CANISTER_ID 14 | 15 | export const createActor = (canisterId, options = {}) => { 16 | const agent = options.agent || new HttpAgent({ ...options.agentOptions }) 17 | 18 | if (options.agent && options.agentOptions) { 19 | console.warn( 20 | "Detected both agent and agentOptions passed to createActor. Ignoring agentOptions and proceeding with the provided agent." 21 | ) 22 | } 23 | 24 | // Fetch root key for certificate validation during development 25 | if (process.env.DFX_NETWORK !== "ic") { 26 | agent.fetchRootKey().catch(err => { 27 | console.warn( 28 | "Unable to fetch root key. Check to ensure that your local replica is running" 29 | ) 30 | console.error(err) 31 | }) 32 | } 33 | 34 | // Creates an actor with using the candid interface and the HttpAgent 35 | return Actor.createActor(idlFactory, { 36 | agent, 37 | canisterId, 38 | ...options.actorOptions 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs_ic_template", 3 | "version": "0.6.0", 4 | "author": "Henry Chan", 5 | "description": "Next.js Internet Computer Starter Template", 6 | "keywords": [ 7 | "nextjs", 8 | "internet computer", 9 | "icp", 10 | "starter", 11 | "dfinity" 12 | ], 13 | "scripts": { 14 | "dev": "next dev", 15 | "build": "next build", 16 | "start": "next start", 17 | "lint": "next lint", 18 | "lint:fix": "next lint --fix", 19 | "export": "next build", 20 | "declaration:generate": "dfx generate", 21 | "sync:hello": "DFX_NETWORK=local && rsync -avr .dfx/$(echo ${DFX_NETWORK:-'**'})/canisters/hello --exclude='idl/' --exclude='*.wasm' --delete ui/declarations", 22 | "sync:image": "DFX_NETWORK=local && rsync -avr .dfx/$(echo ${DFX_NETWORK:-'**'})/canisters/image --exclude='idl/' --exclude='*.wasm' --delete ui/declarations" 23 | }, 24 | "devDependencies": { 25 | "eslint": "^9.21.0", 26 | "eslint-config-next": "^15.2.0", 27 | "eslint-plugin-react": "^7.33.2", 28 | "eslint-plugin-react-hooks": "^5.1.0" 29 | }, 30 | "dependencies": { 31 | "@dfinity/agent": "^2.3.0", 32 | "@dfinity/candid": "^2.3.0", 33 | "@dfinity/principal": "^2.3.0", 34 | "assert": "^2.1.0", 35 | "buffer": "^6.0.3", 36 | "compressorjs": "^1.2.1", 37 | "events": "^3.3.0", 38 | "next": "^15.2.0", 39 | "react": "^19.0.0", 40 | "react-dom": "^19.0.0", 41 | "react-dropzone": "^14.2", 42 | "stream-browserify": "^3.0.0", 43 | "util": "^0.12.5" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ui/components/ImageSection.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | import { useState } from "react" 3 | 4 | import { resizeImage, fileToCanisterBinaryStoreFormat } from "../utils/image" 5 | import { useDropzone } from "react-dropzone" 6 | 7 | import { makeImageActor } from "../service/actor-locator" 8 | 9 | import { useImageObject } from "../hooks/useImageObject" 10 | 11 | const ImageMaxWidth = 2048 12 | 13 | export const ImageSection = () => { 14 | const [imageId, setImageId] = useState(null) 15 | const [loading, setLoading] = useState("") 16 | const [file, setFile] = useState(null) 17 | 18 | const imgSrc = useImageObject(imageId) 19 | 20 | const { getRootProps, getInputProps } = useDropzone({ 21 | maxFiles: 1, 22 | accept: { 23 | "image/png": [".png"], 24 | "image/jpeg": [".jpg", ".jpeg"] 25 | }, 26 | onDrop: async acceptedFiles => { 27 | if (acceptedFiles.length > 0) { 28 | try { 29 | const firstFile = acceptedFiles[0] 30 | const newFile = await resizeImage(firstFile, ImageMaxWidth) 31 | setFile(newFile) 32 | } catch (error) { 33 | console.error(error) 34 | } 35 | } 36 | } 37 | }) 38 | 39 | async function submitImage() { 40 | if (file == null) { 41 | return 42 | } 43 | 44 | setLoading("Submitting...") 45 | setImageId(null) 46 | 47 | const fileArray = await fileToCanisterBinaryStoreFormat(file) 48 | const imageActor = makeImageActor() 49 | const newImageId = await imageActor.create(fileArray) 50 | setImageId(newImageId) 51 | 52 | setLoading("") 53 | } 54 | 55 | return ( 56 |
57 |
58 |

Image

59 | 60 | 64 | 65 |
66 |
67 |
{loading}
68 | 69 | {imgSrc && canister-image} 70 |
71 |
72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js Internet Computer Starter Template 2 | 3 | This project provides a simple starter template for Dfinity Internet Computer using Next.js framework as frontend. 4 | 5 | **The Most Recent Updates** 6 | 7 | - NextJS 15.2.0 8 | - DFX 0.25.0 9 | - NodeJS 22.12.0 10 | 11 | **Backend** 12 | 13 | - A simple greeting hello world canister written in Motoko 14 | - ImageBucket canister written in Motoko with create image, delete image and getImageById 15 | 16 | **Frontend** 17 | 18 | - A simple React HTML form with name input, sending it to greet canister and showing the returned result 19 | - An Image Upload HTML form with Pick an Image button, upload the image to image canister, loading the image back from the canister and display it using useImageObject React Hook 20 | 21 | ## Live Demo in IC Mainnet 🥳 22 | 23 | https://u4gun-5aaaa-aaaah-qabma-cai.raw.ic0.app 24 | 25 | ![Screenshot](/public/demo-screenshot.png) 26 | 27 | ## Quick Start (Run locally) 28 | 29 | Install: 30 | 31 | - NodeJS 18.\* or higher https://nodejs.org/en/download/ 32 | - Internet Computer dfx CLI https://internetcomputer.org/docs/current/developer-docs/setup/install/ 33 | - Visual Studio Code (Recommended Code Editor) https://code.visualstudio.com/Download 34 | - VSCode extension - Motoko (Recommended) https://marketplace.visualstudio.com/items?itemName=dfinity-foundation.vscode-motoko 35 | 36 | ```bash 37 | sh -ci "$(curl -fsSL https://internetcomputer.org/install.sh)" 38 | ``` 39 | 40 | Clone this Git repository: 41 | 42 | ```bash 43 | git clone https://github.com/dappblock/nextjs-ic-starter 44 | ``` 45 | 46 | Open command terminal: 47 | Enter the commands to start dfx local server in background: 48 | 49 | ```bash 50 | cd nextjs-ic-starter 51 | dfx start --background 52 | ``` 53 | 54 | Note: If you run it in MacOS, you may be asked to allow connections from dfx local server. 55 | 56 | Enter the commands to install dependencies, deploy canister and run Next.js dev server: 57 | 58 | ```bash 59 | npm install 60 | dfx deploy --network local 61 | npm run dev 62 | ``` 63 | 64 | http://localhost:3000/ 65 | 66 | Cleanup - stop dfx server running in background: 67 | 68 | ```bash 69 | dfx stop 70 | ``` 71 | 72 | ## Project Structure 73 | 74 | Internet Computer has the concept of [Canister](https://smartcontracts.org/docs/current/concepts/canisters-code/) which is a computation unit. This project has 3 canisters: 75 | 76 | - hello (backend) 77 | - image (backend) 78 | - hello_assets (frontend) 79 | 80 | Canister configurations are stored in dfx.json. 81 | 82 | ### Backend 83 | 84 | Backend code is inside /backend/ written in [Motoko language](https://internetcomputer.org/docs/current/motoko/main/motoko-introduction). Motoko is a type-safe language with modern language features like async/await and actor build-in. It also has [Orthogonal persistence](https://internetcomputer.org/docs/current/motoko/main/motoko/#orthogonal-persistence) which I find very interesting. 85 | 86 | Image canister is introduced from release v0.2.0. It makes use of orthogonal persistence through stable variables and provides functions for create, delete and get image. See /backend/service/Image.mo. 87 | 88 | ### Frontend 89 | 90 | Frontend code follows Next.js folder convention with /pages storing page React code, /public storing static files including images. This project uses CSS modules for styling which is stored in /ui/styles. React Components are stored in /ui/components 91 | 92 | Entry page code is inside /pages/index.js where the magic starts. With the DFX UI declarations generated code, frontend can use RPC style call to server side actor and its functions without worrying about HTTP request and response parsing. 93 | 94 | To generate UI declarations: 95 | 96 | ``` 97 | dfx generate 98 | ``` 99 | 100 | It will generate files in src/declarations for each canister. In our case, it is image, hello and hello_assets but we only need the backend canister image and hello UI declarations here. 101 | 102 | The next step is to adapt it to work with Next.js. 103 | The final adapted code is in ui/declaration/hello/index.js. 104 | You can also follow the steps below to update it. 105 | 106 | Basically, copy image.did.js and index.js from src/declarations/image/ 107 | 108 | ``` 109 | cp src/declarations/image/image.did.js ui/declarations/image/image.did.js 110 | cp src/declarations/image/index.js ui/declarations/image/index.js 111 | ``` 112 | 113 | Repeat the same for hello. 114 | 115 | ``` 116 | cp src/declarations/hello/hello.did.js ui/declarations/hello/hello.did.js 117 | cp src/declarations/hello/index.js ui/declarations/hello/index.js 118 | ``` 119 | 120 | The next step is to update the canister ID env variable in each canister index.js to use NEXT_PUBLIC prefix so that NextJS can recognize when compiling it. 121 | 122 | Open ui/declarations/hello/index.js and look for the line: 123 | 124 | ``` 125 | export const canisterId = process.env.HELLO_CANISTER_ID; 126 | ``` 127 | 128 | Update HELLO_CANISTER_ID to NEXT_PUBLIC_HELLO_CANISTER_ID: 129 | 130 | ``` 131 | export const canisterId = process.env.NEXT_PUBLIC_HELLO_CANISTER_ID 132 | ``` 133 | 134 | To see the final code, check the original ui/declarations in the Git repo. 135 | 136 | The generated UI declarations also support TypeScript if you prefer TypeScript. 137 | 138 | We use a service locator pattern through actor-locator.js that will handle the dfx agent host using env var NEXT_PUBLIC_IC_HOST. 139 | 140 | Creating hello actor: 141 | 142 | ```javascript 143 | import { makeHelloActor } from "../ui/service/actor-adapter" 144 | const hello = makeHelloActor() 145 | ``` 146 | 147 | Calling hello actor: 148 | 149 | ```javascript 150 | const greeting = await hello.greet(name) 151 | ``` 152 | 153 | The beautiful part is you can invoke the hello actor greet function with async/await style as if they are on the same platform. For details, see React Components GreetingSection.js and ImageSection.js in /ui/components/. 154 | 155 | Webpack configuration: 156 | In Next.js, it's located in next.config.js. 157 | 158 | ## React Hook 159 | 160 | By using React Hook with actor UI declaration, it can greatly simplify frontend dev. It encourages component based composable logic. A great example is useImageObject.js React Hook in /ui/hooks. Given an imageId, useImageObject can load the image binary and convert it to a HTML image source object ready for use in . 161 | 162 | If you look closer, useImageObject.js depends on image-serivce.js which depends on actor-locator.js. When you open ImageSection.js, you can find how useImageObject is being used to greatly reduce the complexity and the underlying calls with Canister. This is the pattern I used very often in my Content Fly Dapp project. 163 | 164 | ## Backend dev 165 | 166 | After marking changes in backend code e.g main.mo in /backend/service/hello, you can deploy it to the local DFX server using: 167 | 168 | ```bash 169 | dfx deploy hello 170 | ``` 171 | 172 | **hello** is the backend canister name defined in dfx.json. 173 | 174 | ## Frontend dev - Next.js Static Code 175 | 176 | Next.js developers are familiar with the handy hot code deployed in the Next.js dev environment when making changes in frontend code. 177 | 178 | After deploying your backend code as shown above, you can run Next.js local dev server **npm run dev** and edit your frontend code with all the benefits of hot code deploy. 179 | 180 | One thing to note is we use Next.js static code export here for hosting in Internet Computer so we can't use any features of Next.js that require server side NodeJS. Potentially, there might be ways to use Internet Computer canister as backend while deploying Next.js dapp to a hosting like Vercel that supports NodeJS server in the future. Further research is needed on that aspect. However, if you do want to run everything decentralized on blockchain including the frontend, you would want to deploy the exported static code to Internet Computer as well. 181 | 182 | ## Deploy and run frontend in local DFX server 183 | 184 | In order to simulate the whole Internet Computer experience, you can deploy and run frontend code to local DFX server by running: 185 | 186 | ```bash 187 | dfx start --background 188 | npm run build 189 | dfx deploy hello_assets 190 | ``` 191 | 192 | **hello_assets** is the frontend canister defined in dfx.json. 193 | 194 | **npm run build** builds and export Next.js as static code storing in **/out** folder which would be picked up by **dfx deploy hello_assets** as defined in dfx.json with **/out** as the source. 195 | 196 | When it completes, you can open Chrome and browse to: 197 | http://localhost:8000/?canisterId=[canisterId] 198 | 199 | Replace [canisterId] with the hello_assets canister ID which you can find by running: 200 | 201 | ```bash 202 | dfx canister id hello_assets 203 | ``` 204 | 205 | ## Environment Configuration 206 | 207 | There are three key configs following Next.js [Environment Variables](https://nextjs.org/docs/basic-features/environment-variables) configuration: 208 | 209 | **.env.development** stores configs for use in local dev. 210 | 211 | ``` 212 | NEXT_PUBLIC_IC_HOST=http://localhost:8000 213 | ``` 214 | 215 | **.env.production** is used when building and exporting static code using **npm run build** 216 | 217 | ``` 218 | NEXT_PUBLIC_IC_HOST=http://localhost:8000 219 | ``` 220 | 221 | Notice both files are identical if we want the Next.js dapp to interact with the local dfx server. 222 | 223 | Note **NEXT_PUBLIC** is the prefix used by Next.js to make env vars available to client side code through [build time inlining](https://nextjs.org/docs/basic-features/environment-variables). 224 | 225 | **.env.icprod** is included for deployment to Internet Computer ic network which would be covered below. 226 | 227 | ## Deploy to IC Network Canister 228 | 229 | The most exciting part is to deploy your Next.js / Internet Computer Dapp to production Internet Computer mainnet blockchain network. 230 | 231 | To do that you will need: 232 | 233 | - ICP tokens and convert it to [cycles](https://internetcomputer.org/docs/current/concepts/tokens-cycles/) 234 | - Cycles wallet 235 | 236 | Follow the [Network Deployment](https://internetcomputer.org/docs/current/developer-docs/setup/cycles/cycles-wallet/) guide to create a wallet. 237 | Dfinity offers [free cycle](https://faucet.dfinity.org/) to developers. 238 | 239 | Now, you can deploy your Next.js Dapp to Internet Computer IC network by adding **--network ic** to the dfx subcommand. We will first update our env var to point to IC network host. Then deploy the backend canister first, export Next.js static code and deploy frontend canister **hello_assets**. 240 | 241 | ```bash 242 | cp .env.icprod .env.production 243 | dfx deploy --network ic 244 | ``` 245 | 246 | Open Chrome and go to https://[canisterId].raw.ic0.app/ 247 | Replace [canisterId] by the hello_assets canister id in the IC network. You can find it by running: 248 | 249 | ```bash 250 | dfx canister --network ic id hello_assets 251 | ``` 252 | 253 | Congratulations !! Well Done !! 👏 🚀 🎉 254 | 255 | ## Troubleshooting 256 | 257 | Use Chrome Dev Tools / Console / Network. Check if the dapp uses the right canister id and hostname. 258 | 259 | ## Author 260 | 261 | Henry Chan, henry@contentfly.app 262 | Twitter: @kinwo 263 | 264 | ## Contributing 265 | 266 | Please feel free to raise an issue or submit a pull request. 267 | 268 | ## License 269 | 270 | MIT 271 | --------------------------------------------------------------------------------