Skip to main content

3 posts tagged with "tutorial"

View All Tags

· 9 min read

blue skies filled of stars

Photo by Billy Huynh on Unsplash


Introduction

As a frontend JavaScript developer stepping into the decentralized web, you may have encountered numerous solutions for Web3 development. However, these solutions often focus on wallet integration and transaction execution, creating a learning curve and deviating from the familiar Web2 development experience.

But fear not! There's a solution that bridges the gap between Web2 and Web3 seamlessly - Juno.

In this blog post, we will explore how to leverage the power of Vue and Juno to develop decentralized applications (dApps). By combining these technologies, you'll discover a user-friendly and efficient approach to building powerful dApps that harness the potential of Web3. Join us on this journey as we unveil the secrets of Juno and empower you to create remarkable decentralized experiences with ease!


Foreword

In my previous blog posts, I discussed similar solutions for React and Angular, two popular JavaScript frontend frameworks. If either of these frameworks is your preferred choice, I encourage you to explore those specific posts for tailored insights.


How Juno Works

If you're not yet acquainted with Juno, it's a powerful open-source Blockchain-as-a-Service platform designed to make decentralized application development more accessible. Think of it as a serverless platform, similar to popular services like Google Firebase or AWS Amplify, but with the added advantage of blockchain technology. Juno takes care of everything by running your applications entirely on the blockchain, ensuring a fully decentralized and secure infrastructure.

By leveraging the Internet Computer blockchain network and infrastructure, Juno introduces a unique concept called "Satellites" for each app you create. These Satellites act as robust smart contracts that encapsulate your entire application, including web assets like JavaScript, HTML, and image files, as well as a simple database, file storage, and authentication mechanisms. With Juno, you have complete control over your app's functionality and data.


Build A Dapp

Let's start building our first decentralized application, or "dapp" for short. In this example, we will create a note-taking app that allows users to store and retrieve data entries, as well as upload files.


Introduction

This tutorial and code sample utilize the Vue Composition API.


Initialization

Before you can integrate Juno into your app, you'll need to create a satellite. This process is explained in detail in the documentation.

Moreover, you also need to install the SDK.

npm i @junobuild/core

After completing both of these steps, you can initialize Juno with your satellite ID at the root of your Vue app, for example in App.vue. This will configure the library to communicate with your smart contract.

<script setup lang="ts">
import { onMounted } from "vue";
import { initJuno } from "@junobuild/core";

onMounted(
async () =>
await initJuno({
satelliteId: "pycrs-xiaaa-aaaal-ab6la-cai",
})
);
</script>

<template>
<h1>Hello World</h1>
</template>

That's it for the configuration! Your app is now ready for Web3! 😎


Authentication

In order to ensure secure and anonymous user identification, it is necessary for users to sign in and sign out. To accomplish this, you can bind the relevant functions to call-to-action buttons placed anywhere within your application.

<script setup lang="ts">
import { signIn, signOut } from "@junobuild/core";
</script>

<button @click="signIn">Sign-in</button>
<button @click="signOut">Sign-out</button>

In order to establish seamless integration with other services, the library and satellite components automatically generate a new entry within your smart contract upon successful user sign-in. This functionality empowers the library to efficiently verify permissions during any data exchange.

To monitor and gain insights into this entry, and consequently access information about the user's state, Juno offers an observable function called authSubscribe(). You have the flexibility to utilize this function as many times as necessary. However, you can also create a store that effectively propagates the user information throughout your app.

import { ref, type Ref } from "vue";
import { defineStore } from "pinia";
import { authSubscribe, type User } from "@junobuild/core";

export const useAuthStore = defineStore("auth", () => {
const user: Ref<User | null | undefined> = ref(undefined);

const unsubscribe = authSubscribe((u) => (user.value = u));

return { user, unsubscribe };
});

By doing so, it becomes convenient to subscribe to it at the top level of your application.

<script setup lang="ts">
import { useAuthStore } from "../stores/auth.store";
import { storeToRefs } from "pinia";

const store = useAuthStore();
const { user } = storeToRefs(store);
</script>

<template>
<template v-if="user !== undefined && user !== null">
<slot />
</template>

<template v-else>
<p>Not signed in.</p>
</template>
</template>

Storing Documents

Juno presents a feature called "Datastore" designed for storing data directly on the blockchain. A Datastore comprises a set of collections, where each collection holds documents, uniquely identified by a key of your choice.

In this tutorial, our objective is to store notes, so it is essential to create a collection in accordance with the instructions provided in the documentation. Choose an appropriate name for the collection, such as "notes."

Once you have set up your application and created the necessary collection, you can utilize the library's setDoc function to persist data onto the blockchain. This function enables you to store your notes securely and immutably.

import { setDoc } from "@junobuild/core";

// TypeScript example from the documentation
await setDoc<Example>({
collection: "my_collection_key",
doc: {
key: "my_document_key",
data: myExample,
},
});

Since the documents in the collection are identified by a unique key, we create keys using nanoid - a tiny string ID generator for JavaScript.

<script lang="ts" setup>
import { ref } from "vue";
import { setDoc } from "@junobuild/core";
import { nanoid } from "nanoid";

const inputText = ref("");

const add = async () => {
const key = nanoid();

await setDoc({
collection: "notes",
doc: {
key,
data: {
text: inputText.value,
},
},
});
};
</script>

<template>
<textarea
rows="5"
placeholder="Your diary entry"
v-model="inputText"
></textarea>

<button @click="add">Add</button>
</template>

Please note that for the sake of simplicity, the code snippets provided in this tutorial do not include proper error handling nor complex form handling.


Listing Documents

In order to retrieve the collection of documents stored on the blockchain, we can utilize the library's listDocs function. This versatile function allows for the inclusion of various parameters to facilitate data filtering, ordering, or pagination.

For the purpose of this tutorial, we will keep the example minimalistic. Our objective is to simply list all user data when a component is mounted.

<script lang="ts" setup>
import { listDocs } from "@junobuild/core";
import { onMounted, ref } from "vue";

const items = ref([]);

const list = async () => {
const { items: data } = await listDocs({
collection: "notes",
});

items.value = data;
};

onMounted(async () => await list());
</script>

<template>
<p v-for="(item, index) in items">
<span> {{ index + 1 }} </span>
<span>{{ item.data.text }}</span>
</p>
</template>

Uploading File

Storing data on the decentralized web can be a complex task. However, Juno is designed to simplify this process for app developers who require easy storage and retrieval of user-generated content, such as photos or files.

When it comes to handling documents, the first step is to create a collection by following the instructions provided in the documentation. In this tutorial, we will focus on implementing image uploads, so the collection can be appropriately named as "images."

To ensure uniqueness and proper identification of stored data, each file is assigned a unique file name and path. This is essential because the data is served on the web, and each piece of data should have a distinct URL.

To accomplish this, we can create a key using a combination of the user's unique ID in its textual representation and a timestamp for each uploaded file. By accessing the property we previously declared in a store, we can retrieve the corresponding user's key.

<script lang="ts" setup>
import { ref } from "vue";
import { useAuthStore } from "@/stores/auth.store";
import { storeToRefs } from "pinia";
import { uploadFile } from "@junobuild/core";

const file = ref(undefined);

const store = useAuthStore();
const { user } = storeToRefs(store);

const setFile = (f) => (file.value = f);

const upload = async () => {
// Demo purpose therefore edge case not properly handled
if ([null, undefined].includes(user.value)) {
return;
}

const filename = `${user.value.key}-${file.value.name}`;

const { downloadUrl } = await uploadFile({
collection: "images",
data: file.value,
filename,
});

console.log("Uploaded", downloadUrl);
};
</script>

<template>
<input type="file" @change="(event) => setFile(event.target.files?.[0])" />

<button @click="upload">Upload</button>
</template>

Once an asset is uploaded, a downloadUrl is returned which provides a direct HTTPS link to access the uploaded asset on the web.


Listing Assets

To retrieve the collection of assets stored on the blockchain, we can utilize the listAssets function provided by the library. This function offers flexibility in terms of parameters, allowing us to filter, order, or paginate the files as needed.

Similar to the previous example with documents, we will keep this example minimalistic.

<script lang="ts" setup>
import { listAssets } from "@junobuild/core";
import { onMounted, ref } from "vue";

const assets = ref([]);

const list = async () => {
const { assets: images } = await listAssets({
collection: "images",
});

assets.value = images;
};

onMounted(async () => await list());
</script>

<template>
<img loading="lazy" :src="asset.downloadUrl" v-for="asset in assets" />
</template>

Deployment 🚀

After you have developed and built your application, the next step is to deploy it on the blockchain. To do this, you need to install the Juno command line interface (CLI) by executing the following command in your terminal:

npm i -g @junobuild/cli

After the installation process is finished, you can gain access to your satellite by following the instructions in the documentation and logging in from the terminal. This will enable your machine to control your satellite.

juno login

Finally, you can deploy your project using the following command:

juno deploy

Congratulations! Your Vue dapp is now live and fully powered by the blockchain 🎉.


Resources


👋

Thank you for reading! Stay connected with Juno by following us on Twitter to keep up with our latest updates.

And if you made it this far, we’d love to have you join the Juno community on Discord. 😉

⭐️⭐️⭐️ are also much appreciated: visit the GitHub repo and show your support!

· 11 min read

Macbook Pro

Photo by Maxwell Nelson on Unsplash


Introduction

There are various Web3 development solutions with unique advantages and limitations. If you are a frontend JavaScript developer seeking to build on the decentralized web, you may find it challenging to navigate. Fortunately, Juno provides a unique approach that combines Web3 power with the ease and familiarity of Web2 development.

After exploring how to combine React and Juno to develop a dApp in a previous blog post, we are now going to take a look at how to develop an Angular app on blockchain.

So, let’s dive in and discover how you can build powerful and user-friendly decentralized applications with Angular!


How Juno Works

If you’re not familiar with Juno, it’s an open-source Blockchain-as-a-Service platform that enables you to build decentralized applications with ease. Think of it as a serverless platform, like Google Firebase or AWS Amplify, but with the added benefits of blockchain technology. With Juno, everything runs on the blockchain, providing a fully decentralized and secure infrastructure for your applications.

Juno leverages the Internet Computer blockchain network and infrastructure to launch a “Satellite” for each app you build, essentially a powerful smart contract that contains your entire app. From the assets provided on the web, such as JavaScript, HTML, and image files, to its state saved in a super simple database, file storage, and authentication, each Satellite is controlled solely by you and contains everything it needs to run smoothly.


Build Your First Dapp

Let’s start building our first decentralized application, or “dapp” for short. In this example, we’ll be creating a note-taking app that enables users to store and retrieve data entries, as well as upload files.


Initialization

Before you can integrate Juno into your Angular app, you’ll need to create a satellite. This process is explained in detail in the documentation.

Moreover, you also need to install the SDK.

npm i @junobuild/core

After completing both of these steps, you can initialize Juno with your satellite ID in the main component of your Angular app. This will configure the library to communicate with your smart contract.

import { Component, OnInit } from "@angular/core";
import { initJuno } from "@junobuild/core";

@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"],
})
export class AppComponent implements OnInit {
async ngOnInit() {
await initJuno({
satelliteId: "pycrs-xiaaa-aaaal-ab6la-cai",
});
}
}

That’s it for the configuration! Your app is now ready for Web3! 😎


Authentication

To enable secure and anonymous user identification, users will need to sign in and sign out. You can bind the corresponding functions to call-to-action buttons anywhere in your app.

import { Component } from "@angular/core";
import { signIn, signOut } from "@junobuild/core";

@Component({
selector: "app-demo",
template: `<button (click)="signIn()">Sign-in</button>
<button (click)="signOut()">Sign-out</button>`,
standalone: true,
})
export class DemoComponent {
readonly signOut = signOut;
readonly signIn = signIn;
}

To integrate tightly with other services, the library and satellite automatically create a new entry in your smart contract when a user successfully signs in. This enables the library to check permissions on any exchange of data.

To observe this entry and, consequently, understand the user’s state, Juno offers an observable function called authSubscribe(). You can use this function as many times as needed, but it’s convenient to create a service that provides the information. This way, you can derive RxJS Observable to propagate the user.

import { Injectable } from "@angular/core";
import { authSubscribe, User } from "@junobuild/core";
import { map, Observable } from "rxjs";

@Injectable({
providedIn: "root",
})
export class AuthService {
readonly user$: Observable<User | null> = new Observable((observer) =>
authSubscribe((user) => observer.next(user))
);

readonly signedIn$: Observable<boolean> = this.user$.pipe(
map((user) => user !== null)
);
}

Juno’s library is designed to be framework-agnostic and currently doesn’t include any framework-specific code. However, we encourage contributions from the community, and if you’re interested in providing Angular bindings, we welcome your contributions to the project! 💪


Storing Documents

Juno offers a feature called “Datastore” for storing data on the blockchain. A Datastore consists of a list of collections that hold your documents, each identified by a key that you define.

In this tutorial, we aim to store notes, so you’ll need to create a collection following the instructions in the documentation and name it accordingly (e.g., “notes”).

After setting up your app and creating the collection, you can use the setDoc function provided by the library to persist data on the blockchain.

import { setDoc } from "@junobuild/core";

// TypeScript example from the documentation
await setDoc<Example>({
collection: "my_collection_key",
doc: {
key: "my_document_key",
data: myExample,
},
});

Since the documents in the collection are identified by a unique key, we create keys using nanoid — a tiny string ID generator for JavaScript.

import { Component } from "@angular/core";
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
import { setDoc } from "@junobuild/core";
import { nanoid } from "nanoid";
import { Entry } from "../../types/entry";

@Component({
selector: "app-input",
template: ` <form (ngSubmit)="onSubmit()" [formGroup]="entryForm">
<textarea formControlName="entry"></textarea>

<button [disabled]="entryForm.disabled">Submit</button>
</form>`,
standalone: true,
imports: [ReactiveFormsModule],
})
export class InputComponent {
entryForm = this.formBuilder.group({
entry: "",
});

constructor(private formBuilder: FormBuilder) {}

async onSubmit() {
await this.save();
}

private async save() {
const key = nanoid();

await setDoc<Entry>({
collection: "notes",
doc: {
key,
data: {
text: this.entryForm.value.entry,
},
},
});
}
}

Please note that for the sake of simplicity, the code snippets provided in this tutorial do not include proper error handling nor complex form handling.

In the above code snippet, we are persisting an object called “entries” to the blockchain. For the purposes of this tutorial, we have declared the type of the object in our frontend code as “Entry”. The documents are persisted on the blockchain as blobs, which means that you can persist any structure that can be serialized.

export interface Entry {
text: string;
url?: string;
}

Listing Documents

To retrieve the list of documents saved on the blockchain, we can use the listDocs function provided by the library. This function can accept various parameters to filter, order, or paginate the data.

In this tutorial, we simply list all data of the users while observing the authentication state with the service we declared previously. If a user is set, we fetch the data; if none, we reset the entries. This is possible because every time the user signs in or out, the state will automatically be reflected.

In addition, we create also a dedicated service to keep the data in memory for reusability purposes. This service includes a reload function, which can be useful to reload the data as needed.

import { Inject, Injectable } from "@angular/core";
import type { Doc } from "@junobuild/core";
import { listDocs } from "@junobuild/core";
import type { Observable } from "rxjs";
import {
combineLatest,
from,
map,
of,
shareReplay,
startWith,
Subject,
switchMap,
} from "rxjs";
import type { Entry } from "../types/entry";
import { AuthService } from "./auth.service";

@Injectable({
providedIn: "root",
})
export class DocsService {
private reloadSubject = new Subject<void>();

docs$: Observable<Doc<Entry>[]> = combineLatest([
this.authService.user$,
this.reloadSubject.pipe(startWith(undefined)),
]).pipe(
switchMap(([user, _]) => {
if (user === null) {
return of([]);
}

return from(
listDocs<Entry>({
collection: "notes",
filter: {},
})
).pipe(map(({ items }) => items));
}),
startWith([]),
shareReplay({ bufferSize: 1, refCount: true })
);

constructor(@Inject(AuthService) private readonly authService: AuthService) {}

reload() {
this.reloadSubject.next();
}
}

For display purposes, we can subscribe to the asynchronous stream as we would with any observable.

import { Component, Inject } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { type Doc } from "@junobuild/core";
import { Observable } from "rxjs";
import { DocsService } from "../../services/docs.service";
import type { Entry } from "../../types/entry";

@Component({
selector: "app-list",
template: `<p *ngFor="let doc of docs$ | async">
{{ doc.key }}: {{ doc.data.text }}
</p>`,
imports: [BrowserModule],
standalone: true,
})
export class ListComponent {
readonly docs$: Observable<Doc<Entry>[]> = this.docsService.docs$;

constructor(@Inject(DocsService) private readonly docsService: DocsService) {}
}

Uploading File

Storing user-generated content on Web3 can be a challenge, but Juno makes it easy for app developers. It’s designed to enable the storage and serving of assets such as photos or videos effortlessly.

To upload files, you’ll need to create a collection by following the instructions in the documentation. In this tutorial, we’ll focus on image uploads, so the collection should be named “images.”

Each file stored on the blockchain is identified by a unique filename and path that corresponds to a unique URL. To accomplish this, we create a key using a combination of the user’s unique ID in text form and a timestamp for each uploaded file. We can retrieve the corresponding user’s key by accessing the observable of the authentication service we declared in previous chapter.

import { Component, Inject } from "@angular/core";
import { AssetKey, uploadFile, User } from "@junobuild/core";
import { filter, from, switchMap, take } from "rxjs";
import { AuthService } from "../../services/auth.service";
import { BrowserModule } from "@angular/platform-browser";

@Component({
selector: "app-upload",
template: `
<input
type="file"
accept="image/png, image/gif, image/jpeg"
(change)="onFileChanged($event)"
/>

<img *ngIf="downloadUrl !== undefined" [src]="downloadUrl" loading="lazy" />

<button (click)="add()">Upload</button>
`,
standalone: true,
imports: [BrowserModule],
})
export class UploadComponent {
private file: File | undefined;

downloadUrl: string | undefined;

constructor(@Inject(AuthService) private readonly authService: AuthService) {}

add() {
this.authService.user$
.pipe(
filter((user) => user !== null),
switchMap((user) => from(this.upload(user as User))),
take(1)
)
.subscribe(({ downloadUrl }) => {
this.downloadUrl = downloadUrl;
});
}

onFileChanged($event: Event) {
const target = $event.target as HTMLInputElement;
this.file = target.files?.[0];
}

private async upload(user: User): Promise<AssetKey> {
const filename = `${user.key}-${this.file.name}`;

return uploadFile({
collection: "images",
data: this.file,
filename,
});
}
}

Once an asset is uploaded, a downloadUrl is returned which provides a direct HTTPS link to access the uploaded asset on the web.


Listing Assets

To fetch the list of assets saved on the blockchain, we can use the listAssets function provided by the library. This function can accept various parameters to filter, order, or paginate the files.

Similar to the documents, we can create a service that converts the list into an observable.

import { Inject, Injectable } from "@angular/core";
import type { Assets } from "@junobuild/core";
import { listAssets } from "@junobuild/core";
import type { Observable } from "rxjs";
import {
combineLatest,
from,
map,
of,
shareReplay,
startWith,
Subject,
switchMap,
} from "rxjs";
import { AuthService } from "./auth.service";

@Injectable({
providedIn: "root",
})
export class AssetsService {
private reloadSubject = new Subject<void>();

assets$: Observable<Assets[]> = combineLatest([
this.authService.user$,
this.reloadSubject.pipe(startWith(undefined)),
]).pipe(
switchMap(([user, _]) => {
if (user === null) {
return of([]);
}

return from(
listAssets({
collection: "images",
filter: {},
})
).pipe(map(({ assets }) => assets));
}),
startWith([]),
shareReplay({ bufferSize: 1, refCount: true })
);

constructor(@Inject(AuthService) private readonly authService: AuthService) {}

reload() {
this.reloadSubject.next();
}
}

For display purposes, we also subscribe to the asynchronous stream as we would with any observable.

import { Component, Inject } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { Asset } from "@junobuild/core";
import { Observable } from "rxjs";
import { AssetsService } from "../../services/assets.service";

@Component({
selector: "app-assets",
template: `<img
*ngFor="let asset of assets$ | async"
[src]="asset.downloadUrl"
loading="lazy"
/>`,
imports: [BrowserModule],
standalone: true,
})
export class AssetsComponent {
readonly assets$: Observable<Asset[]> = this.assetsService.assets$;

constructor(
@Inject(AssetsService) private readonly assetsService: AssetsService
) {}
}

Deployment 🚀

Once your application is developed and built, the next step is to launch it on the blockchain. To accomplish this, you must install the Juno command line interface by executing the following command in your terminal:

npm i -g @junobuild/cli

After the installation process is finished, you can gain access to your satellite by following the instructions in the documentation and logging in from the terminal. This will enable your machine to control your satellite.

juno login

Finally, you can deploy your project using the following command:

juno deploy

Congratulations! Your Angular app is now decentralized 🎉.


Resources


👋

Thank you for reading! Follow me on Twitter for more exciting coding content.

And if you made it this far, we’d love to have you join the Juno community on Discord. 😉

⭐️⭐️⭐️ are also much appreciated: visit the GitHub repo and show your support!

· 9 min read

Working on Sunday

Photo by Jantine Doornbos on Unsplash


Introduction

There are numerous solutions available for building on Web3, each with their own unique advantages and limitations, but most are often related to connecting wallets and executing transactions. However, if you are a frontend JavaScript developer looking to build on the decentralized web, you may have found that the development experience can be quite different from the familiar world of Web2.

That’s why Juno takes a different approach, aiming to harness the power of Web3 without sacrificing the ease and familiarity of Web2 development.

In this blog post, we’ll explore how to combine React and Juno to develop a dApp. So, let’s dive in and discover how Juno can help you build powerful and user-friendly decentralized applications!


How Juno Works

Juno is an open-source Blockchain-as-a-Service platform. It works just like traditional serverless platforms such as Google Firebase or AWS Amplify, but with one key difference: everything on Juno runs on the blockchain. This means that you get a fully decentralized and secure infrastructure for your applications, which is pretty cool if you ask me.

Behind the scenes, Juno uses the Internet Computer blockchain network and infrastructure to launch what we call a “Satellite” for each app you build. A Satellite is essentially a smart contract on steroids that contains your entire app. From its assets provided on the web (such as JavaScript, HTML, and image files) to its state saved in a super simple database, file storage, and authentication, each Satellite controlled solely by you contains everything it needs to run smoothly.


Build Your First DApp

Let’s build our first dapp! In this example, we will create a simple note-taking app that allows users to store data entries, upload files, and retrieve them as needed.


Initialization

Before you can integrate Juno into your ReactJS app, you’ll need to create a satellite. This process is explained in detail in the documentation.

Moreover, you also need to install the SDK.

npm i @junobuild/core

After completing both of these steps, you can initialize Juno with your satellite ID at the top of your React app. This will configure the library to communicate with your smart contract.

import { useEffect } from "react";
import { initJuno } from "@junobuild/core";

function App() {
useEffect(() => {
(async () =>
await initJuno({
satelliteId: "pycrs-xiaaa-aaaal-ab6la-cai",
}))();
}, []);

return <h1>Hello World</h1>;
}

export default App;

That’s it for the configuration! Your app is now ready for Web3! 😎


Authentication

To securely identify users anonymously, they will need to sign in and sign out. You can bind the related functions to call-to-actions anywhere in your app.

import { signIn, signOut } from "@junobuild/core";

<button
type="button"
onClick={signIn}
>Login</button>

<button
type="button"
onClick={signOut}
>Logout</button>

To integrate tightly with other services, the library and satellite automatically create a new entry in your smart contract when a user successfully signs in. This enables the library to check permissions on any exchange of data.

To observe this entry and, by extension, get to know the user’s state, Juno provides an observable function called authSubscribe(). You can use it as many times as required, but I find it convenient to subscribe to it at the top of an app. This way, we can create a Context to propagate the user.

import { createContext, useEffect, useState } from "react";
import { authSubscribe } from "@junobuild/core";

export const AuthContext = createContext();

export const Auth = ({ children }) => {
const [user, setUser] = useState(undefined);

useEffect(() => {
const sub = authSubscribe((user) => setUser(user));

return () => unsubscribe();
}, []);

return (
<AuthContext.Provider value={{ user }}>
{user !== undefined && user !== null ? (
<div>{children}</div>
) : (
<p>Not signed in.</p>
)}
</AuthContext.Provider>
);
};

Juno’s library is framework-agnostic and currently does not include any framework-specific code. However, we welcome contributions from the community. If you are interested in providing React plugins, contexts, hooks or else, feel free to contribute to the project! 💪


Storing Documents

Storing data on the blockchain with Juno is done through a feature called “Datastore”. A datastore consists of a list of collections that contain your documents, each identified by a textual key that you define.

In this tutorial, our goal is to store notes. To achieve this, you will need to follow the instructions in the documentation to create a collection, which can be named accordingly (“notes”).

Once your app is set up and your collection is created, we can persist data on the blockchain using the setDoc function provided by the library.

import { setDoc } from "@junobuild/core";

// TypeScript example from the documentation
await setDoc<Example>({
collection: "my_collection_key",
doc: {
key: "my_document_key",
data: myExample,
},
});

Since the documents in the collection are identified by a unique key, we create keys using nanoid — a tiny string ID generator for JavaScript.

import { useState } from "react";
import { setDoc } from "@junobuild/core";
import { nanoid } from "nanoid";

export const Example = () => {
const [inputText, setInputText] = useState("");

const add = async () => {
await setDoc({
collection: "data",
doc: {
key: nanoid(),
data: {
text: inputText,
},
},
});
};

return (
<>
<textarea
onChange={(e) => setInputText(e.target.value)}
value={inputText}
></textarea>

<button type="button" onClick={add}>
Add
</button>
</>
);
};

Listing Documents

To fetch the list of documents saved on the blockchain, we can use the listDocs function provided by the library. This function can accept various parameters to filter, order, or paginate the data.

In this tutorial, we’ll keep the example minimal. We simply list all data of the users while observing the Context we declared previously. If a user is set, we fetch the data; if none, we reset the entries. This is possible because every time the user signs in or out, the state will automatically be reflected.

import { useContext, useEffect, useState } from "react";
import { AuthContext } from "./Auth";
import { listDocs } from "@junobuild/core";

export const ListExample = () => {
const { user } = useContext(AuthContext);

const [items, setItems] = useState([]);

const list = async () => {
const { items } = await listDocs({
collection: "notes",
filter: {},
});

setItems(items);
};

useEffect(() => {
if ([undefined, null].includes(user)) {
setItems([]);
return;
}

(async () => await list())();
}, [user]);

return (
<>
{items.map(({ key, data: { text } }) => (
<p key={key}>{text}</p>
))}
</>
);
};

Uploading File

Storing data on the decentralized web isn’t always that easy. Fortunately, Juno is not one of those and is designed for app developers who need to store and serve user-generated content, such as photos or videos, with ease.

As for the documents, to upload assets you will need first to follow the instructions in the documentation to create a collection. In this tutorial we will implement image uploads, so the collection can be named accordingly (“images”).

The stored data are identified with unique file names and paths. This is because the data are provided on the web and therefore each piece of data should match a unique URL.

To achieve this, we can create a key using a combination of the unique user’s ID in its textual representation and a timestamp for each file uploaded. We can get access the property we passed down through the Context in the previous chapter to retrieve the corresponding user’s key.

import { useContext, useState } from "react";
import { AuthContext } from "./Auth";
import { uploadFile } from "@junobuild/core";

export const UploadExample = () => {
const [file, setFile] = useState();
const [image, setImage] = useState();

const { user } = useContext(AuthContext);

const add = async () => {
const filename = `${user.key}-${file.name}`;

const { downloadUrl } = await uploadFile({
collection: "images",
data: file,
filename,
});

setImage(downloadUrl);
};

return (
<>
<input
type="file"
accept="image/png, image/gif, image/jpeg"
onChange={(event) => setFile(event.target.files?.[0])}
/>

<button type="button" onClick={add}>
Add
</button>

{image !== undefined && <img src={image} loading="lazy" />}
</>
);
};

Once an asset is uploaded, a downloadUrl is returned which provides a direct HTTPS link to access the uploaded asset on the web.


Listing Assets

To fetch the list of assets saved on the blockchain, we can use the listAssets function provided by the library. This function can accept various parameters to filter, order, or paginate the files.

As for the documents previously, we’ll keep the example minimal. We simply list all assets of the users observing the Context.

import { useContext, useEffect, useState } from "react";
import { AuthContext } from "./Auth";
import { listAssets } from "@junobuild/core";

export const ListAssetsExample = () => {
const { user } = useContext(AuthContext);

const [assets, setAssets] = useState([]);

const list = async () => {
const { assets } = await listAssets({
collection: "images",
filter: {},
});

setAssets(assets);
};

useEffect(() => {
if ([undefined, null].includes(user)) {
setAssets([]);
return;
}

(async () => await list())();
}, [user]);

return (
<>
{assets.map(({ fullPath, downloadUrl }) => (
<img key={fullPath} loading="lazy" src={downloadUrl} />
))}
</>
);
};

Deployment 🚀

After you have developed and built your application, you can launch it on the blockchain. To do this, you will need to install the Juno command line interface by running the following command in your terminal:

npm i -g @junobuild/cli

Once the installation is complete, you can log in to your satellite from the terminal using the instructions in the documentation. This will grant control of your machine to your satellite:

juno login

Finally, you can deploy your project using the following command:

juno deploy

Congratulations! Your app is now decentralized 🎉.


Ressources


👋

Thank you for reading! Follow us on Twitter to stay up to date.

And, if you’ve read this far, you should definitely join us on Discord. 😉