├── app
├── .nvmrc
├── src
│ ├── vite-env.d.ts
│ ├── index.css
│ ├── main.tsx
│ ├── types
│ │ └── types.ts
│ ├── components
│ │ ├── common
│ │ │ ├── ContentWrapper.tsx
│ │ │ └── Loader.tsx
│ │ ├── login
│ │ │ └── ContentWrapper.tsx
│ │ ├── button
│ │ │ ├── button.tsx
│ │ │ └── LoginButton.tsx
│ │ ├── post
│ │ │ ├── comment.tsx
│ │ │ ├── extendedPost.tsx
│ │ │ ├── createCommentPopup.tsx
│ │ │ └── createPostPopup.tsx
│ │ ├── feed
│ │ │ ├── post.tsx
│ │ │ └── feed.tsx
│ │ ├── auth
│ │ │ └── auth.tsx
│ │ ├── loader
│ │ │ └── loader.tsx
│ │ ├── header
│ │ │ └── header.tsx
│ │ └── error
│ │ │ └── errorPopup.tsx
│ ├── pages
│ │ ├── login
│ │ │ └── index.tsx
│ │ ├── home
│ │ │ └── index.tsx
│ │ ├── post
│ │ │ └── [id].tsx
│ │ └── feed
│ │ │ └── FeedPage.tsx
│ ├── App.tsx
│ ├── api
│ │ ├── clientApi.ts
│ │ ├── httpClient.ts
│ │ └── dataSource
│ │ │ └── ClientApiDataSource.ts
│ ├── constants
│ │ └── en.global.json
│ └── assets
│ │ └── react.svg
├── tsconfig.json
├── postcss.config.cjs
├── vite.config.ts
├── .gitignore
├── tailwind.config.js
├── index.html
├── tsconfig.app.json
├── eslint.config.js
├── public
│ └── vite.svg
├── package.json
└── README.md
├── .gitignore
├── package.json
├── logic
├── Cargo.toml
├── build.sh
├── src
│ └── lib.rs
└── Cargo.lock
├── .husky
└── pre-commit
├── pnpm-lock.yaml
├── .github
└── workflows
│ ├── setup-node
│ └── action.yml
│ └── deploy-react.yml
└── README.md
/app/.nvmrc:
--------------------------------------------------------------------------------
1 | v18.20.2
--------------------------------------------------------------------------------
/app/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # misc
2 | .DS_Store
3 |
4 | node_modules
5 | logic/target/
6 |
--------------------------------------------------------------------------------
/app/src/index.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 |
3 | body {
4 | background-color: #0c0d0e;
5 | }
6 |
--------------------------------------------------------------------------------
/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [{ "path": "./tsconfig.app.json" }]
4 | }
5 |
--------------------------------------------------------------------------------
/app/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [require("@tailwindcss/postcss"), require("autoprefixer")],
3 | };
4 |
--------------------------------------------------------------------------------
/app/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import tailwindcss from "@tailwindcss/vite";
3 | import react from "@vitejs/plugin-react-swc";
4 |
5 | // https://vite.dev/config/
6 | export default defineConfig({
7 | plugins: [react(), tailwindcss()],
8 | });
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "prepare": "husky"
4 | },
5 | "devDependencies": {
6 | "husky": "^9.0.11"
7 | },
8 | "packageManager": "pnpm@9.6.0+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e"
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from "react";
2 | import { createRoot } from "react-dom/client";
3 | import App from "./App.tsx";
4 |
5 | import "./index.css";
6 | import "@near-wallet-selector/modal-ui/styles.css";
7 |
8 | createRoot(document.getElementById("root")!).render(
9 |
10 |
11 | ,
12 | );
13 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/app/src/types/types.ts:
--------------------------------------------------------------------------------
1 | export interface Comment {
2 | text: string;
3 | user: string;
4 | }
5 |
6 | export interface Post {
7 | id: string;
8 | title: string;
9 | content: string;
10 | comments: Comment[];
11 | }
12 |
13 | export interface JsonWebToken {
14 | context_id: string;
15 | token_type: string;
16 | exp: number;
17 | sub: string;
18 | executor_public_key: string;
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/components/common/ContentWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default function ContentWrapper({
4 | children,
5 | }: {
6 | children: React.ReactNode;
7 | }) {
8 | return (
9 |
10 |
11 | {children}
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/components/login/ContentWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default function ContentWrapper({
4 | children,
5 | }: {
6 | children: React.ReactNode;
7 | }) {
8 | return (
9 |
10 |
11 | {children}
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/logic/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "only-peers"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [lib]
7 | crate-type = ["cdylib"]
8 |
9 | [dependencies]
10 | calimero-sdk = { git = "https://github.com/calimero-network/core", branch = "master" }
11 |
12 | [profile.app-release]
13 | inherits = "release"
14 | codegen-units = 1
15 | opt-level = "z"
16 | lto = true
17 | debug = false
18 | panic = "abort"
19 | overflow-checks = true
--------------------------------------------------------------------------------
/app/src/pages/login/index.tsx:
--------------------------------------------------------------------------------
1 | import { useNavigate } from "react-router-dom";
2 | import { ClientLogin } from "@calimero-network/calimero-client";
3 |
4 | import ContentWrapper from "../../components/common/ContentWrapper";
5 |
6 | export default function LoginPage() {
7 | const navigate = useNavigate();
8 | return (
9 |
10 | navigate("/feed")} />
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/logic/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | cd "$(dirname $0)"
5 |
6 | TARGET="${CARGO_TARGET_DIR:-target}"
7 |
8 | rustup target add wasm32-unknown-unknown
9 |
10 | cargo build --target wasm32-unknown-unknown --profile app-release
11 |
12 | mkdir -p res
13 |
14 | cp $TARGET/wasm32-unknown-unknown/app-release/only_peers.wasm ./res/
15 |
16 | if command -v wasm-opt > /dev/null; then
17 | wasm-opt -Oz ./res/only_peers.wasm -o ./res/only_peers.wasm
18 | fi
19 |
--------------------------------------------------------------------------------
/app/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: [
3 | "./index.html",
4 | "./src/**/*.{js,jsx,ts,tsx}", // Make sure to include your source folder
5 | ],
6 | theme: {
7 | extend: {
8 | backgroundImage: {
9 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
10 | "gradient-conic":
11 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
12 | },
13 | },
14 | },
15 | plugins: [],
16 | };
17 |
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Calimero | Only Peers
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | # Check for changes in the 'app' directory
5 | if git diff --cached --name-only | grep -q '^app/'; then
6 | echo "Running checks for the React app..."
7 | (cd app && pnpm prettier && pnpm lint:fix)
8 | fi
9 |
10 | # Check for changes in the 'logic' directory
11 | if git diff --cached --name-only | grep -q '^logic/'; then
12 | echo "Running checks for the Rust logic..."
13 | (cd logic && cargo test && cargo fmt -- --check)
14 | fi
--------------------------------------------------------------------------------
/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '9.0'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | importers:
8 |
9 | .:
10 | devDependencies:
11 | husky:
12 | specifier: ^9.0.11
13 | version: 9.0.11
14 |
15 | packages:
16 |
17 | husky@9.0.11:
18 | resolution: {integrity: sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==}
19 | engines: {node: '>=18'}
20 | hasBin: true
21 |
22 | snapshots:
23 |
24 | husky@9.0.11: {}
25 |
--------------------------------------------------------------------------------
/.github/workflows/setup-node/action.yml:
--------------------------------------------------------------------------------
1 | name: setup-node
2 | description: "Setup Node.js ⚙️ - Cache dependencies ⚡ - Install dependencies 🔧"
3 | runs:
4 | using: "composite"
5 | steps:
6 | - name: Setup Node.js ⚙️
7 | uses: actions/setup-node@v4
8 | with:
9 | node-version-file: "/app/.nvmrc"
10 |
11 | - name: Install pnpm
12 | run: npm install -g pnpm
13 | shell: bash
14 |
15 | - name: Install dependencies 🔧
16 | shell: bash
17 | run: pnpm install
18 |
19 | inputs:
20 | working-directory:
21 | description: "Working directory"
22 | default: "./app"
--------------------------------------------------------------------------------
/app/src/pages/home/index.tsx:
--------------------------------------------------------------------------------
1 | import { useNavigate } from "react-router-dom";
2 | import { getAccessToken } from "@calimero-network/calimero-client";
3 | import { useEffect } from "react";
4 | import Loading from "../../components/common/Loader";
5 |
6 | function Home() {
7 | const navigate = useNavigate();
8 | const accessToken = getAccessToken();
9 |
10 | useEffect(() => {
11 | if (!accessToken) {
12 | navigate("/login");
13 | } else {
14 | navigate("/feed");
15 | }
16 | }, [accessToken, navigate]);
17 |
18 | return (
19 | <>
20 |
21 | >
22 | );
23 | }
24 |
25 | export default Home;
26 |
--------------------------------------------------------------------------------
/app/src/components/button/button.tsx:
--------------------------------------------------------------------------------
1 | interface ButtonProps {
2 | title: string;
3 | onClick: () => void;
4 | backgroundColor: string;
5 | backgroundColorHover: string;
6 | disabled?: boolean;
7 | }
8 |
9 | export default function Button({
10 | title,
11 | onClick,
12 | backgroundColor,
13 | backgroundColorHover,
14 | disabled = false,
15 | }: ButtonProps) {
16 | return (
17 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/components/post/comment.tsx:
--------------------------------------------------------------------------------
1 | import { UserIcon } from "@heroicons/react/24/solid";
2 | import { Comment } from "../../types/types";
3 |
4 | interface CommentProps {
5 | commentItem: Comment;
6 | }
7 |
8 | export default function CommentComponent({ commentItem }: CommentProps) {
9 | return (
10 |
11 |
12 |
13 |
{commentItem.user}
14 |
15 |
16 | {commentItem.text}
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/components/button/LoginButton.tsx:
--------------------------------------------------------------------------------
1 | interface ButtonProps {
2 | title: string;
3 | onClick: () => void;
4 | backgroundColor: string;
5 | textColor: string;
6 | disabled?: boolean;
7 | icon: React.ReactNode;
8 | }
9 |
10 | export default function LoginButton({
11 | title,
12 | onClick,
13 | backgroundColor,
14 | textColor,
15 | disabled = false,
16 | icon,
17 | }: ButtonProps) {
18 | return (
19 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/app/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 | "target": "ES2020",
5 | "useDefineForClassFields": true,
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "isolatedModules": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "noUncheckedSideEffectImports": true,
24 | "noImplicitAny": false
25 | },
26 | "include": ["src"]
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { Route, BrowserRouter, Routes } from "react-router-dom";
2 | import { AccessTokenWrapper } from "@calimero-network/calimero-client";
3 |
4 | import Home from "./pages/home";
5 | import LoginPage from "./pages/login";
6 | import FeedPage from "./pages/feed/FeedPage";
7 | import PostPage from "./pages/post/[id]";
8 |
9 | function App() {
10 | return (
11 |
12 |
13 |
14 | } />
15 | } />
16 | } />
17 | } />
18 |
19 |
20 |
21 | );
22 | }
23 |
24 | export default App;
25 |
--------------------------------------------------------------------------------
/app/src/api/clientApi.ts:
--------------------------------------------------------------------------------
1 | import { ApiResponse } from "@calimero-network/calimero-client";
2 | import { Comment, Post } from "../types/types";
3 |
4 | export interface PostRequest {
5 | id: number;
6 | }
7 |
8 | export interface CreatePostRequest {
9 | title: string;
10 | content: string;
11 | }
12 |
13 | export interface CreateCommentRequest {
14 | post_id: number;
15 | text: string;
16 | user: string;
17 | }
18 |
19 | export enum ClientMethod {
20 | CREATE_COMMENT = "create_comment",
21 | POST = "post",
22 | CREATE_POST = "create_post",
23 | POSTS = "posts",
24 | }
25 |
26 | export interface ClientApi {
27 | fetchFeed(): ApiResponse;
28 | fetchPost(params: PostRequest): ApiResponse;
29 | createPost(params: CreatePostRequest): ApiResponse;
30 | createComment(params: CreateCommentRequest): ApiResponse;
31 | }
32 |
--------------------------------------------------------------------------------
/app/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from "@eslint/js";
2 | import globals from "globals";
3 | import reactHooks from "eslint-plugin-react-hooks";
4 | import reactRefresh from "eslint-plugin-react-refresh";
5 | import tseslint from "typescript-eslint";
6 |
7 | export default tseslint.config(
8 | { ignores: ["dist"] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ["**/*.{ts,tsx}"],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | "react-hooks": reactHooks,
18 | "react-refresh": reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | "react-refresh/only-export-components": [
23 | "warn",
24 | { allowConstantExport: true },
25 | ],
26 | "@typescript-eslint/no-explicit-any": "off",
27 | },
28 | },
29 | );
30 |
--------------------------------------------------------------------------------
/app/src/components/feed/post.tsx:
--------------------------------------------------------------------------------
1 | import { ChatBubbleLeftIcon } from "@heroicons/react/24/outline";
2 | import { Post } from "../../types/types";
3 | import { useNavigate } from "react-router-dom";
4 |
5 | export interface PostProps {
6 | post: Post;
7 | }
8 |
9 | export default function PostFeed({ post }: PostProps) {
10 | const navigate = useNavigate();
11 | return (
12 |
13 |
navigate(`/post/${post.id}`)}
16 | >
17 |
{post.title}
18 |
{post.content}
19 |
20 |
21 |
22 | {post.comments.length}
23 |
24 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/components/auth/auth.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import {
3 | getAccessToken,
4 | getAppEndpointKey,
5 | getApplicationId,
6 | getRefreshToken,
7 | } from "@calimero-network/calimero-client";
8 | import { useLocation, useNavigate } from "react-router-dom";
9 |
10 | interface AuthProps {
11 | children: React.ReactNode;
12 | }
13 |
14 | export default function WithIdAuth({ children }: AuthProps) {
15 | const navigate = useNavigate();
16 | const location = useLocation();
17 |
18 | useEffect(() => {
19 | const url = getAppEndpointKey();
20 | const applicationId = getApplicationId();
21 | const accessToken = getAccessToken();
22 | const refreshToken = getRefreshToken();
23 |
24 | if (!url || !applicationId) {
25 | if (!location.pathname.startsWith("/login")) {
26 | navigate("/login");
27 | }
28 | } else if (!accessToken || !refreshToken) {
29 | if (!location.pathname.startsWith("/auth")) {
30 | navigate("/auth");
31 | }
32 | } else if (location.pathname.startsWith("/auth")) {
33 | navigate("/feed");
34 | }
35 | }, [navigate]);
36 |
37 | return <>{children}>;
38 | }
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Only Peers
2 |
3 | ## Introduction
4 |
5 | This project is a demo of Only Peers with Calimero Network. More information about Only Peers can be found in our [docs](https://calimero-network.github.io/tutorials/awesome-projects/only-peers/).
6 |
7 | ## Logic
8 |
9 | ```bash title="Terminal"
10 | cd logic
11 | ```
12 |
13 | ```bash title="Terminal"
14 | chmod +x ./build.sh
15 | ```
16 |
17 | ```bash title="Terminal"
18 | ./build.sh
19 | ```
20 |
21 | ## React Vite App
22 |
23 | ```bash title="Terminal"
24 | cd app
25 | ```
26 |
27 | ```bash title="Terminal"
28 | pnpm install
29 | ```
30 |
31 | ```bash title="Terminal"
32 | pnpm build
33 | ```
34 |
35 | ```bash title="Terminal"
36 | pnpm dev
37 | ```
38 |
39 | ## Setup
40 |
41 | - Start your node with auth enabled - [docs](https://calimero-network.github.io/build/quickstart)
42 | - Follow instruction to create context for your node - [instructions](https://calimero-network.github.io/tutorials/install-application/#create-new-context)
43 | - Open app in browser on default url `http://localhost:5174/only-peers-client/` and follow login instructions.
44 |
45 | For more information how to build app check our docs:
46 | https://calimero-network.github.io/build/quickstart
47 |
--------------------------------------------------------------------------------
/app/src/constants/en.global.json:
--------------------------------------------------------------------------------
1 | {
2 | "errorPopup": {
3 | "dialogTitle": "Error",
4 | "reloadButtonText": "Reload the page",
5 | "logoutButtonText": "Logout"
6 | },
7 | "feedPage": {
8 | "feedTitle": "Posts",
9 | "createButtonText": "Create a post",
10 | "noPostsText": "No posts yet"
11 | },
12 | "header": {
13 | "logoText": "OnlyPeers",
14 | "executorPk": "Executor Public Key",
15 | "addButtonText": "Add peerId",
16 | "logoutButtonText": "Logout"
17 | },
18 | "loader": {
19 | "srOnlyText": "Loading..."
20 | },
21 | "commentPopup": {
22 | "loadingText": "Creating Comment...",
23 | "dialogTitle": "Add a comment",
24 | "inputLabel": "Comment",
25 | "inputPlaceholder": "content",
26 | "maxCharLength": "/250",
27 | "cancelButtonText": "Cancel",
28 | "createButtonText": "Comment"
29 | },
30 | "postPopup": {
31 | "loadingText": "Creating Post...",
32 | "dialogTitle": "Create a post",
33 | "inputTitleLabel": "Post Title",
34 | "inputTitlePlaceholder": "title",
35 | "inputContentLabel": "Post Content",
36 | "inputContentPlacerholder": "content",
37 | "maxCharLength": "/250",
38 | "backButtonText": "Back to the feed",
39 | "createButtonText": "Create Post"
40 | },
41 | "extendedPost": {
42 | "addButtonText": "Add a comment"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/components/common/Loader.tsx:
--------------------------------------------------------------------------------
1 | export default function Loading() {
2 | return (
3 |
4 |
20 |
Loading...
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/components/loader/loader.tsx:
--------------------------------------------------------------------------------
1 | import translations from "../../constants/en.global.json";
2 |
3 | export default function Loader() {
4 | const t = translations.loader;
5 | return (
6 |
7 |
23 |
{t.srOnlyText}
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/components/feed/feed.tsx:
--------------------------------------------------------------------------------
1 | import PostFeed from "./post";
2 | import CreatePostPopup from "../post/createPostPopup";
3 | import translations from "../../constants/en.global.json";
4 | import Button from "../button/button";
5 | import { Post } from "../../types/types";
6 |
7 | interface FeedProps {
8 | posts: Post[];
9 | createPost: (title: string, content: string) => void;
10 | openCreatePost: boolean;
11 | setOpenCreatePost: (open: boolean) => void;
12 | }
13 |
14 | export default function Feed({
15 | posts,
16 | createPost,
17 | openCreatePost,
18 | setOpenCreatePost,
19 | }: FeedProps) {
20 | const t = translations.feedPage;
21 | return (
22 |
23 |
24 |
25 |
26 |
{t.feedTitle}
27 |
34 | {posts.length === 0 ? (
35 |
36 | {t.noPostsText}
37 |
38 | ) : (
39 |
40 | {posts.map((post, id) => (
41 |
42 | ))}
43 |
44 | )}
45 |
46 | {openCreatePost && (
47 |
52 | )}
53 |
54 |
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "only-peers-client",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build",
9 | "preview": "vite preview",
10 | "lint": "concurrently \"pnpm:lint:*(!fix)\"",
11 | "lint:src": "eslint . --ext .ts,.tsx src --report-unused-disable-directives --max-warnings 0",
12 | "lint:fix": "concurrently \"pnpm:lint:*:fix\"",
13 | "lint:src:fix": "eslint . --ext .ts,.tsx --fix src",
14 | "predeploy": "pnpm run build",
15 | "deploy": "gh-pages -d build",
16 | "prettier": "exec prettier . --write",
17 | "prepare": "husky"
18 | },
19 | "dependencies": {
20 | "@calimero-network/calimero-client": "1.6.4",
21 | "@headlessui/react": "^2.2.2",
22 | "@heroicons/react": "^2.2.0",
23 | "@near-wallet-selector/account-export": "^8.9.5",
24 | "@near-wallet-selector/core": "^8.9.5",
25 | "@near-wallet-selector/modal-ui": "^8.9.5",
26 | "@near-wallet-selector/my-near-wallet": "^8.9.5",
27 | "@near-wallet-selector/near-wallet": "^8.9.3",
28 | "@tailwindcss/postcss": "^4.0.17",
29 | "@tailwindcss/vite": "^4.0.17",
30 | "autoprefixer": "^10.4.21",
31 | "axios": "^1.8.4",
32 | "concurrently": "^7.3.0",
33 | "postcss": "^8.5.3",
34 | "react": "^18",
35 | "react-dom": "^18",
36 | "react-router-dom": "^7.4.0",
37 | "tailwindcss": "^4.0.17"
38 | },
39 | "devDependencies": {
40 | "@eslint/js": "^9.21.0",
41 | "@types/react": "^18.3.20",
42 | "@types/react-dom": "^18.3.5",
43 | "@vitejs/plugin-react-swc": "^3.8.0",
44 | "eslint": "^9.21.0",
45 | "eslint-plugin-react-hooks": "^5.1.0",
46 | "eslint-plugin-react-refresh": "^0.4.19",
47 | "globals": "^15.15.0",
48 | "gh-pages": "^6.1.1",
49 | "husky": "^9.0.11",
50 | "prettier": "^3.5.3",
51 | "typescript": "~5.7.2",
52 | "typescript-eslint": "^8.24.1",
53 | "vite": "^6.4.1"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## Expanding the ESLint configuration
11 |
12 | If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
13 |
14 | ```js
15 | export default tseslint.config({
16 | extends: [
17 | // Remove ...tseslint.configs.recommended and replace with this
18 | ...tseslint.configs.recommendedTypeChecked,
19 | // Alternatively, use this for stricter rules
20 | ...tseslint.configs.strictTypeChecked,
21 | // Optionally, add this for stylistic rules
22 | ...tseslint.configs.stylisticTypeChecked,
23 | ],
24 | languageOptions: {
25 | // other options...
26 | parserOptions: {
27 | project: ["./tsconfig.node.json", "./tsconfig.app.json"],
28 | tsconfigRootDir: import.meta.dirname,
29 | },
30 | },
31 | });
32 | ```
33 |
34 | You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
35 |
36 | ```js
37 | // eslint.config.js
38 | import reactX from "eslint-plugin-react-x";
39 | import reactDom from "eslint-plugin-react-dom";
40 |
41 | export default tseslint.config({
42 | plugins: {
43 | // Add the react-x and react-dom plugins
44 | "react-x": reactX,
45 | "react-dom": reactDom,
46 | },
47 | rules: {
48 | // other rules...
49 | // Enable its recommended typescript rules
50 | ...reactX.configs["recommended-typescript"].rules,
51 | ...reactDom.configs.recommended.rules,
52 | },
53 | });
54 | ```
55 |
--------------------------------------------------------------------------------
/app/src/components/post/extendedPost.tsx:
--------------------------------------------------------------------------------
1 | import { ChatBubbleLeftIcon } from "@heroicons/react/24/outline";
2 | import { Post } from "../../types/types";
3 | import CommentComponent from "./comment";
4 | import CreateCommentPopup from "./createCommentPopup";
5 | import translations from "../../constants/en.global.json";
6 | import Button from "../button/button";
7 |
8 | interface ExtendedPostProps {
9 | post: Post | null;
10 | createComment: (content: string) => void;
11 | openCreateComment: boolean;
12 | setOpenCreateComment: (openCreateComment: boolean) => void;
13 | }
14 |
15 | export default function ExtendedPost({
16 | post,
17 | createComment,
18 | openCreateComment,
19 | setOpenCreateComment,
20 | }: ExtendedPostProps) {
21 | const t = translations.extendedPost;
22 | return (
23 |
24 | {post && (
25 | <>
26 |
27 |
28 |
{post.title}
29 |
30 | {post.content}
31 |
32 |
33 |
34 |
35 | {post.comments.length}
36 |
37 |
38 |
39 |
46 |
47 | {post.comments.map((commentItem, id) => (
48 |
49 |
50 |
51 | ))}
52 |
53 |
54 |
55 | {openCreateComment && (
56 |
61 | )}
62 | >
63 | )}
64 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/app/src/pages/post/[id].tsx:
--------------------------------------------------------------------------------
1 | import Header from "../../components/header/header";
2 | import ExtendedPost from "../../components/post/extendedPost";
3 | import { useCallback, useEffect, useState } from "react";
4 | import Loader from "../../components/loader/loader";
5 | import ErrorPopup from "../../components/error/errorPopup";
6 | import { Post } from "../../types/types";
7 | import { ClientApiDataSource } from "../../api/dataSource/ClientApiDataSource";
8 | import { CreateCommentRequest, PostRequest } from "../../api/clientApi";
9 | import { useParams } from "react-router-dom";
10 | import { getJWTObject } from "@calimero-network/calimero-client";
11 |
12 | export default function PostPage() {
13 | const { id } = useParams();
14 | const [error, setError] = useState("");
15 | const [post, setPost] = useState(null);
16 | const [openCreateComment, setOpenCreateComment] = useState(false);
17 | const postId = id ? parseInt(id as string, 10) : null;
18 | const [loading, setLoading] = useState(true);
19 |
20 | const createComment = async (text: string) => {
21 | const jwt = getJWTObject();
22 | if (!jwt) {
23 | return;
24 | }
25 | const commentRequest: CreateCommentRequest = {
26 | post_id: postId ?? 0,
27 | text: text,
28 | user: jwt.executor_public_key,
29 | };
30 | const result = await new ClientApiDataSource().createComment(
31 | commentRequest,
32 | );
33 | if (result.error) {
34 | setError(result.error.message);
35 | return;
36 | }
37 |
38 | setOpenCreateComment(false);
39 | fetchPost(postId);
40 | };
41 |
42 | const fetchPost = useCallback(async (postId: number | null) => {
43 | if (postId === null) {
44 | return;
45 | }
46 | const postRequest: PostRequest = { id: postId };
47 | const result = await new ClientApiDataSource().fetchPost(postRequest);
48 | if (result.error) {
49 | setError(result.error.message);
50 | return;
51 | }
52 | setPost(result.data);
53 | setLoading(false);
54 | }, []);
55 |
56 | useEffect(() => {
57 | const signGetPostRequest = async () => {
58 | fetchPost(postId);
59 | };
60 | signGetPostRequest();
61 | }, [fetchPost, postId]);
62 |
63 | return (
64 | <>
65 |
66 | {loading && }
67 | {error && }
68 | {!loading && post && (
69 |
75 | )}
76 | >
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-react.yml:
--------------------------------------------------------------------------------
1 | name: Deploy React site to Pages
2 |
3 | on:
4 | push:
5 | branches: ['master']
6 | workflow_dispatch:
7 |
8 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
9 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
10 | concurrency:
11 | group: 'pages'
12 | cancel-in-progress: false
13 |
14 | permissions:
15 | id-token: write
16 | contents: write
17 | pages: write
18 |
19 | defaults:
20 | run:
21 | working-directory: ./app
22 |
23 | jobs:
24 | # Build job
25 | build:
26 | runs-on: ubuntu-latest
27 | steps:
28 | - name: Checkout
29 | uses: actions/checkout@v4
30 |
31 | - name: Setup Node.js ⚙️ - Cache dependencies ⚡ - Install dependencies 🔧
32 | uses: ./.github/workflows/setup-node
33 |
34 | - name: Setup Pages
35 | uses: actions/configure-pages@v5
36 | with:
37 | static_site_generator: ''
38 |
39 | - name: Install dependencies
40 | run: pnpm install
41 |
42 | - name: Build with React
43 | run: pnpm run build
44 |
45 | - name: Upload artifact
46 | uses: actions/upload-pages-artifact@v3
47 | with:
48 | name: github-pages
49 | path: ./app/dist
50 |
51 | deploy:
52 | environment:
53 | name: github-pages
54 | url: ${{ steps.deployment.outputs.page_url }}
55 | runs-on: ubuntu-latest
56 | needs: build
57 | steps:
58 | - name: Download artifact
59 | uses: actions/download-artifact@v4
60 |
61 | - name: Deploy to GitHub Pages
62 | id: deployment
63 | uses: actions/deploy-pages@v4
64 | with:
65 | token: ${{ secrets.GITHUB_TOKEN }}
66 |
67 | notify-on-failure:
68 | runs-on: ubuntu-latest
69 | needs: [build, deploy]
70 | if: failure()
71 | steps:
72 | - name: Notify failure
73 | uses: 'ravsamhq/notify-slack-action@2.5.0'
74 | with:
75 | status: failure
76 | notification_title: 'Deploy failed on ${{ github.ref_name }} - <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Failure>'
77 | message_format: ':fire: *Deploy Only-Peers site to Pages* in <${{ github.server_url }}/${{ github.repository }}/${{ github.ref_name }}|${{ github.repository }}>'
78 | footer: 'Linked Repo <${{ github.server_url }}/${{ github.repository }}|${{ github.repository }}> | <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Failure>'
79 | env:
80 | SLACK_WEBHOOK_URL: ${{ secrets.DEPLOY_FAIL_SLACK }}
81 |
--------------------------------------------------------------------------------
/app/src/components/header/header.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | import translations from "../../constants/en.global.json";
4 |
5 | import Button from "../button/button";
6 | import {
7 | clientLogout,
8 | getAccessToken,
9 | getExecutorPublicKey,
10 | } from "@calimero-network/calimero-client";
11 | import { useNavigate } from "react-router-dom";
12 |
13 | export default function Header() {
14 | const t = translations.header;
15 | const navigate = useNavigate();
16 | const [accessToken] = useState(getAccessToken());
17 | const [executorPublicKey, setExecutorPublicKey] = useState(
18 | null,
19 | );
20 |
21 | useEffect(() => {
22 | const setExecutorPk = async () => {
23 | const publicKey = getExecutorPublicKey();
24 | setExecutorPublicKey(publicKey);
25 | };
26 | if (accessToken) {
27 | setExecutorPk();
28 | }
29 | }, [accessToken]);
30 |
31 | function logout() {
32 | clientLogout();
33 | navigate("/");
34 | }
35 |
36 | return (
37 |
38 |
78 |
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/app/src/pages/feed/FeedPage.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react";
2 | import { Post } from "../../types/types";
3 |
4 | import ErrorPopup from "../../components/error/errorPopup";
5 | import Feed from "../../components/feed/feed";
6 | import Header from "../../components/header/header";
7 | import Loader from "../../components/loader/loader";
8 | import { CreatePostRequest } from "../../api/clientApi";
9 | import { ClientApiDataSource } from "../../api/dataSource/ClientApiDataSource";
10 | import { useNavigate } from "react-router-dom";
11 | import { getAccessToken } from "@calimero-network/calimero-client";
12 |
13 | export default function FeedPage() {
14 | const navigate = useNavigate();
15 | const accessToken = getAccessToken();
16 | const [openCreatePost, setOpenCreatePost] = useState(false);
17 | const [posts, setPosts] = useState([]);
18 | const [error, setError] = useState("");
19 | const [loading, setLoading] = useState(true);
20 |
21 | useEffect(() => {
22 | if (!accessToken) {
23 | navigate("/login");
24 | } else {
25 | navigate("/feed");
26 | }
27 | }, [accessToken, navigate]);
28 |
29 | const fetchFeed = useCallback(async () => {
30 | try {
31 | const response = await new ClientApiDataSource().fetchFeed();
32 | if (response.error) {
33 | setError(response.error.message);
34 | setLoading(false);
35 | }
36 | setPosts(response?.data?.slice().reverse() ?? []);
37 | setLoading(false);
38 | } catch (error: unknown) {
39 | setError(error instanceof Error ? error.message : "Unknown error");
40 | setLoading(false);
41 | }
42 | }, []);
43 |
44 | useEffect(() => {
45 | const signGetPostRequest = async () => {
46 | fetchFeed();
47 | };
48 | signGetPostRequest();
49 | }, [fetchFeed]);
50 |
51 | const createPost = async (title: string, content: string) => {
52 | setError("");
53 | setLoading(true);
54 | const createPostRequest: CreatePostRequest = {
55 | title,
56 | content,
57 | };
58 | const result = await new ClientApiDataSource().createPost(
59 | createPostRequest,
60 | );
61 | if (result.error) {
62 | setError(result.error.message);
63 | setLoading(false);
64 | setOpenCreatePost(false);
65 | return;
66 | }
67 |
68 | setOpenCreatePost(false);
69 | setLoading(false);
70 | fetchFeed();
71 | };
72 |
73 | return (
74 | <>
75 |
76 | {loading && }
77 | {error && }
78 | {!loading && posts && (
79 |
85 | )}
86 | >
87 | );
88 | }
89 |
--------------------------------------------------------------------------------
/logic/src/lib.rs:
--------------------------------------------------------------------------------
1 | use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize};
2 | use calimero_sdk::serde::Serialize;
3 | use calimero_sdk::{app, env};
4 |
5 | #[app::state(emits = for<'a> Event<'a>)]
6 | #[derive(BorshDeserialize, BorshSerialize, Default)]
7 | #[borsh(crate = "calimero_sdk::borsh")]
8 | pub struct OnlyPeers {
9 | posts: Vec,
10 | }
11 |
12 | #[derive(BorshDeserialize, BorshSerialize, Default, Serialize)]
13 | #[borsh(crate = "calimero_sdk::borsh")]
14 | #[serde(crate = "calimero_sdk::serde")]
15 | pub struct Post {
16 | id: usize,
17 | title: String,
18 | content: String,
19 | comments: Vec,
20 | }
21 |
22 | #[derive(BorshDeserialize, BorshSerialize, Default, Serialize)]
23 | #[borsh(crate = "calimero_sdk::borsh")]
24 | #[serde(crate = "calimero_sdk::serde")]
25 | pub struct Comment {
26 | text: String,
27 | user: String,
28 | }
29 |
30 | #[app::event]
31 | pub enum Event<'a> {
32 | PostCreated {
33 | id: usize,
34 | title: &'a str,
35 | content: &'a str,
36 | },
37 | CommentCreated {
38 | post_id: usize,
39 | user: &'a str,
40 | text: &'a str,
41 | },
42 | }
43 |
44 | #[app::logic]
45 | impl OnlyPeers {
46 | #[app::init]
47 | pub fn init() -> OnlyPeers {
48 | OnlyPeers::default()
49 | }
50 |
51 | pub fn post(&self, id: usize) -> Option<&Post> {
52 | env::log(&format!("Getting post with id: {:?}", id));
53 |
54 | self.posts.get(id)
55 | }
56 |
57 | pub fn posts(&self) -> &[Post] {
58 | env::log("Getting all posts");
59 |
60 | &self.posts
61 | }
62 |
63 | pub fn create_post(&mut self, title: String, content: String) -> &Post {
64 | env::log(&format!(
65 | "Creating post with title: {:?} and content: {:?}",
66 | title, content
67 | ));
68 |
69 | app::emit!(Event::PostCreated {
70 | id: self.posts.len(),
71 | // todo! should we maybe only emit an ID, and let notified clients fetch the post?
72 | title: &title,
73 | content: &content,
74 | });
75 |
76 | self.posts.push(Post {
77 | id: self.posts.len(),
78 | title,
79 | content,
80 | comments: Vec::new(),
81 | });
82 |
83 | match self.posts.last() {
84 | Some(post) => post,
85 | None => env::unreachable(),
86 | }
87 | }
88 |
89 | pub fn create_comment(
90 | &mut self,
91 | post_id: usize,
92 | user: String, // todo! expose executor identity to app context
93 | text: String,
94 | ) -> Option<&Comment> {
95 | env::log(&format!(
96 | "Creating comment under post with id: {:?} as user: {:?} with text: {:?}",
97 | post_id, user, text
98 | ));
99 |
100 | let post = self.posts.get_mut(post_id)?;
101 |
102 | app::emit!(Event::CommentCreated {
103 | post_id,
104 | // todo! should we maybe only emit an ID, and let notified clients fetch the comment?
105 | user: &user,
106 | text: &text,
107 | });
108 |
109 | post.comments.push(Comment { user, text });
110 |
111 | post.comments.last()
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/app/src/components/error/errorPopup.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment, useState } from "react";
2 | import { Dialog, Transition } from "@headlessui/react";
3 | import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
4 | import translations from "../../constants/en.global.json";
5 | import Button from "../button/button";
6 | import { clientLogout } from "@calimero-network/calimero-client";
7 | import { useNavigate } from "react-router-dom";
8 |
9 | interface ErrorPopupProps {
10 | error: string;
11 | }
12 |
13 | export default function ErrorPopup(props: ErrorPopupProps) {
14 | const t = translations.errorPopup;
15 | const [open, setOpen] = useState(true);
16 | const navigate = useNavigate();
17 |
18 | const logout = () => {
19 | clientLogout();
20 | navigate("/");
21 | };
22 |
23 | return (
24 |
25 |
90 |
91 | );
92 | }
93 |
--------------------------------------------------------------------------------
/app/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/api/httpClient.ts:
--------------------------------------------------------------------------------
1 | import { ErrorResponse, ResponseData } from "@calimero-network/calimero-client";
2 | import { Axios, AxiosResponse, AxiosError } from "axios";
3 |
4 | export interface Header {
5 | [key: string]: string;
6 | }
7 |
8 | export interface HttpClient {
9 | get(url: string, headers?: Header[]): Promise>;
10 | post(
11 | url: string,
12 | body?: unknown,
13 | headers?: Header[],
14 | ): Promise>;
15 | put(
16 | url: string,
17 | body?: unknown,
18 | headers?: Header[],
19 | ): Promise>;
20 | delete(
21 | url: string,
22 | body?: unknown,
23 | headers?: Header[],
24 | ): Promise>;
25 | patch(
26 | url: string,
27 | body?: unknown,
28 | headers?: Header[],
29 | ): Promise>;
30 | head(url: string, headers?: Header[]): Promise>;
31 | }
32 |
33 | export class AxiosHttpClient implements HttpClient {
34 | private axios: Axios;
35 |
36 | constructor(axios: Axios) {
37 | this.axios = axios;
38 | }
39 |
40 | async get(url: string, headers?: Header[]): Promise> {
41 | return this.request(
42 | this.axios.get>(url, {
43 | headers: headers?.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
44 | }),
45 | );
46 | }
47 |
48 | async post(
49 | url: string,
50 | body?: unknown,
51 | headers?: Header[],
52 | ): Promise> {
53 | return this.request(
54 | this.axios.post>(url, body, {
55 | headers: headers?.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
56 | }),
57 | );
58 | }
59 |
60 | async put(
61 | url: string,
62 | body?: unknown,
63 | headers?: Header[],
64 | ): Promise> {
65 | return this.request(
66 | this.axios.put>(url, body, {
67 | headers: headers?.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
68 | }),
69 | );
70 | }
71 |
72 | async delete(url: string, headers?: Header[]): Promise> {
73 | return this.request(
74 | this.axios.delete>(url, {
75 | headers: headers?.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
76 | }),
77 | );
78 | }
79 |
80 | async patch(
81 | url: string,
82 | body?: unknown,
83 | headers?: Header[],
84 | ): Promise> {
85 | return this.request(
86 | this.axios.patch>(url, body, {
87 | headers: headers?.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
88 | }),
89 | );
90 | }
91 |
92 | async head(url: string, headers?: Header[]): Promise> {
93 | return this.request(
94 | this.axios.head(url, {
95 | headers: headers?.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
96 | }),
97 | );
98 | }
99 |
100 | private async request(
101 | promise: Promise>>,
102 | ): Promise> {
103 | try {
104 | const response = await promise;
105 |
106 | //head does not return body so we are adding data manually
107 | if (response.config?.method?.toUpperCase() === "HEAD") {
108 | return {
109 | data: undefined as unknown as T,
110 | };
111 | } else {
112 | return response.data;
113 | }
114 | } catch (e: unknown) {
115 | if (e instanceof AxiosError) {
116 | //head does not return body so we are adding error manually
117 | if (e.config?.method?.toUpperCase() === "HEAD") {
118 | return {
119 | error: {
120 | code: e.request.status,
121 | message: e.message,
122 | },
123 | };
124 | }
125 |
126 | const error: ErrorResponse = e.response?.data.error;
127 | if (!error || !error.message) {
128 | return {
129 | error: GENERIC_ERROR,
130 | };
131 | }
132 | return {
133 | error: {
134 | code: error.code,
135 | message: error.message,
136 | },
137 | };
138 | }
139 | return {
140 | error: GENERIC_ERROR,
141 | };
142 | }
143 | }
144 | }
145 |
146 | const GENERIC_ERROR: ErrorResponse = {
147 | code: 500,
148 | message: "Something went wrong",
149 | };
150 |
--------------------------------------------------------------------------------
/app/src/components/post/createCommentPopup.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment, useState } from "react";
2 | import { Dialog, Transition } from "@headlessui/react";
3 | import Loader from "../loader/loader";
4 | import translations from "../../constants/en.global.json";
5 | import Button from "../button/button";
6 |
7 | interface CreateCommentPopupProps {
8 | createComment: (text: string) => void;
9 | open: boolean;
10 | setOpen: (open: boolean) => void;
11 | }
12 |
13 | export default function CreateCommentPopup({
14 | createComment,
15 | open,
16 | setOpen,
17 | }: CreateCommentPopupProps) {
18 | const t = translations.commentPopup;
19 | const [text, setText] = useState("");
20 | const [loading, setLoading] = useState(false);
21 |
22 | const onCreateComment = () => {
23 | setLoading(true);
24 | createComment(text);
25 | };
26 |
27 | return (
28 |
29 |
114 |
115 | );
116 | }
117 |
--------------------------------------------------------------------------------
/app/src/components/post/createPostPopup.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment, useState } from "react";
2 | import { Dialog, Transition } from "@headlessui/react";
3 | import Loader from "../loader/loader";
4 | import translations from "../../constants/en.global.json";
5 | import Button from "../button/button";
6 |
7 | interface CreatePostPopupProps {
8 | createPost: (title: string, content: string) => void;
9 | open: boolean;
10 | setOpen: (open: boolean) => void;
11 | }
12 |
13 | export default function CreatePostPopup({
14 | createPost,
15 | open,
16 | setOpen,
17 | }: CreatePostPopupProps) {
18 | const t = translations.postPopup;
19 | const [title, setTitle] = useState("");
20 | const [content, setContent] = useState("");
21 | const [loading, setLoading] = useState(false);
22 |
23 | const onCreatePost = () => {
24 | setLoading(true);
25 | createPost(title, content);
26 | };
27 | return (
28 |
29 |
130 |
131 | );
132 | }
133 |
--------------------------------------------------------------------------------
/app/src/api/dataSource/ClientApiDataSource.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ApiResponse,
3 | getAppEndpointKey,
4 | handleRpcError,
5 | JsonRpcClient,
6 | prepareAuthenticatedRequestConfig,
7 | RpcError,
8 | } from "@calimero-network/calimero-client";
9 |
10 | import {
11 | ClientApi,
12 | ClientMethod,
13 | CreateCommentRequest,
14 | CreatePostRequest,
15 | PostRequest,
16 | } from "../clientApi";
17 | import { Comment, Post } from "../../types/types";
18 |
19 | export function getJsonRpcClient() {
20 | const appEndpointKey = getAppEndpointKey();
21 | if (!appEndpointKey) {
22 | throw new Error(
23 | "Application endpoint key is missing. Please check your configuration.",
24 | );
25 | }
26 | return new JsonRpcClient(appEndpointKey, "/jsonrpc");
27 | }
28 |
29 | export class ClientApiDataSource implements ClientApi {
30 | private async handleError(
31 | error: RpcError,
32 | params: any,
33 | callbackFunction: any,
34 | ) {
35 | if (error && error.code) {
36 | const response = await handleRpcError(error, getAppEndpointKey);
37 | if (response.code === 403) {
38 | return await callbackFunction(params);
39 | }
40 | return {
41 | error: await handleRpcError(error, getAppEndpointKey),
42 | };
43 | }
44 | }
45 |
46 | async fetchFeed(): ApiResponse {
47 | try {
48 | const { config, error, contextId, publicKey } =
49 | prepareAuthenticatedRequestConfig();
50 |
51 | if (error) {
52 | return {
53 | data: null,
54 | error: {
55 | code: error.code,
56 | message: error.message,
57 | },
58 | };
59 | }
60 |
61 | const response = await getJsonRpcClient().execute(
62 | {
63 | contextId,
64 | method: ClientMethod.POSTS,
65 | argsJson: {},
66 | executorPublicKey: publicKey,
67 | },
68 | config,
69 | );
70 |
71 | if (response?.error) {
72 | return await this.handleError(response.error, {}, this.fetchFeed);
73 | }
74 |
75 | return {
76 | data: response.result?.output ?? [],
77 | error: null,
78 | };
79 | } catch (error) {
80 | console.error("fetchFeed failed:", error);
81 | let errorMessage = "An unexpected error occurred during fetchFeed";
82 | if (error instanceof Error) {
83 | errorMessage = error.message;
84 | } else if (typeof error === "string") {
85 | errorMessage = error;
86 | }
87 | return {
88 | error: {
89 | code: 500,
90 | message: errorMessage,
91 | },
92 | };
93 | }
94 | }
95 |
96 | async fetchPost(params: PostRequest): ApiResponse {
97 | try {
98 | const { config, error, contextId, publicKey } =
99 | prepareAuthenticatedRequestConfig();
100 |
101 | if (error) {
102 | return {
103 | data: null,
104 | error: {
105 | code: error.code,
106 | message: error.message,
107 | },
108 | };
109 | }
110 |
111 | const response = await getJsonRpcClient().execute(
112 | {
113 | contextId,
114 | method: ClientMethod.POST,
115 | argsJson: params,
116 | executorPublicKey: publicKey,
117 | },
118 | config,
119 | );
120 |
121 | if (response?.error) {
122 | return await this.handleError(response.error, {}, this.fetchFeed);
123 | }
124 |
125 | if (!response?.result?.output) {
126 | return {
127 | data: null,
128 | error: {
129 | code: 404,
130 | message: "Post not found",
131 | },
132 | };
133 | }
134 |
135 | return {
136 | data: response?.result?.output,
137 | error: null,
138 | };
139 | } catch (error) {
140 | console.error("fetchPost failed:", error);
141 | let errorMessage = "An unexpected error occurred during fetchPost";
142 | if (error instanceof Error) {
143 | errorMessage = error.message;
144 | } else if (typeof error === "string") {
145 | errorMessage = error;
146 | }
147 | return {
148 | error: {
149 | code: 500,
150 | message: errorMessage,
151 | },
152 | };
153 | }
154 | }
155 |
156 | async createPost(params: CreatePostRequest): ApiResponse {
157 | try {
158 | const { config, error, contextId, publicKey } =
159 | prepareAuthenticatedRequestConfig();
160 |
161 | if (error) {
162 | return {
163 | data: null,
164 | error: {
165 | code: error.code,
166 | message: error.message,
167 | },
168 | };
169 | }
170 |
171 | const response = await getJsonRpcClient().execute<
172 | CreatePostRequest,
173 | Post
174 | >(
175 | {
176 | contextId,
177 | method: ClientMethod.CREATE_POST,
178 | argsJson: params,
179 | executorPublicKey: publicKey,
180 | },
181 | config,
182 | );
183 | if (response?.error) {
184 | return await this.handleError(response.error, {}, this.fetchFeed);
185 | }
186 |
187 | if (!response?.result?.output) {
188 | return {
189 | data: null,
190 | error: {
191 | code: 500,
192 | message: "Error creating post",
193 | },
194 | };
195 | }
196 |
197 | return {
198 | data: response?.result?.output,
199 | error: null,
200 | };
201 | } catch (error) {
202 | console.error("createPost failed:", error);
203 | let errorMessage = "An unexpected error occurred during createPost";
204 | if (error instanceof Error) {
205 | errorMessage = error.message;
206 | } else if (typeof error === "string") {
207 | errorMessage = error;
208 | }
209 | return {
210 | error: {
211 | code: 500,
212 | message: errorMessage,
213 | },
214 | };
215 | }
216 | }
217 |
218 | async createComment(params: CreateCommentRequest): ApiResponse {
219 | try {
220 | const { config, error, contextId, publicKey } =
221 | prepareAuthenticatedRequestConfig();
222 |
223 | if (error) {
224 | return {
225 | data: null,
226 | error: {
227 | code: error.code,
228 | message: error.message,
229 | },
230 | };
231 | }
232 |
233 | const response = await getJsonRpcClient().execute<
234 | CreateCommentRequest,
235 | Comment
236 | >(
237 | {
238 | contextId,
239 | method: ClientMethod.CREATE_COMMENT,
240 | argsJson: params,
241 | executorPublicKey: publicKey,
242 | },
243 | config,
244 | );
245 | if (response?.error) {
246 | return await this.handleError(response.error, {}, this.fetchFeed);
247 | }
248 |
249 | if (!response?.result?.output) {
250 | return {
251 | data: null,
252 | error: {
253 | code: 500,
254 | message: "Error creating post",
255 | },
256 | };
257 | }
258 |
259 | return {
260 | data: response?.result?.output,
261 | error: null,
262 | };
263 | } catch (error) {
264 | console.error("createComment failed:", error);
265 | let errorMessage = "An unexpected error occurred during createComment";
266 | if (error instanceof Error) {
267 | errorMessage = error.message;
268 | } else if (typeof error === "string") {
269 | errorMessage = error;
270 | }
271 | return {
272 | error: {
273 | code: 500,
274 | message: errorMessage,
275 | },
276 | };
277 | }
278 | }
279 | }
280 |
--------------------------------------------------------------------------------
/logic/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "borsh"
7 | version = "1.5.1"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed"
10 | dependencies = [
11 | "borsh-derive",
12 | "cfg_aliases",
13 | ]
14 |
15 | [[package]]
16 | name = "borsh-derive"
17 | version = "1.5.1"
18 | source = "registry+https://github.com/rust-lang/crates.io-index"
19 | checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b"
20 | dependencies = [
21 | "once_cell",
22 | "proc-macro-crate",
23 | "proc-macro2",
24 | "quote",
25 | "syn",
26 | "syn_derive",
27 | ]
28 |
29 | [[package]]
30 | name = "calimero-sdk"
31 | version = "0.1.0"
32 | source = "git+https://github.com/calimero-network/core?branch=master#c8a8786898c4a7ca66ae6772cd6d5a333e8e45af"
33 | dependencies = [
34 | "borsh",
35 | "calimero-sdk-macros",
36 | "cfg-if",
37 | "serde",
38 | "serde_json",
39 | ]
40 |
41 | [[package]]
42 | name = "calimero-sdk-macros"
43 | version = "0.1.0"
44 | source = "git+https://github.com/calimero-network/core?branch=master#c8a8786898c4a7ca66ae6772cd6d5a333e8e45af"
45 | dependencies = [
46 | "prettyplease",
47 | "proc-macro2",
48 | "quote",
49 | "syn",
50 | "thiserror",
51 | ]
52 |
53 | [[package]]
54 | name = "cfg-if"
55 | version = "1.0.0"
56 | source = "registry+https://github.com/rust-lang/crates.io-index"
57 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
58 |
59 | [[package]]
60 | name = "cfg_aliases"
61 | version = "0.2.1"
62 | source = "registry+https://github.com/rust-lang/crates.io-index"
63 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
64 |
65 | [[package]]
66 | name = "equivalent"
67 | version = "1.0.1"
68 | source = "registry+https://github.com/rust-lang/crates.io-index"
69 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
70 |
71 | [[package]]
72 | name = "hashbrown"
73 | version = "0.14.5"
74 | source = "registry+https://github.com/rust-lang/crates.io-index"
75 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
76 |
77 | [[package]]
78 | name = "indexmap"
79 | version = "2.4.0"
80 | source = "registry+https://github.com/rust-lang/crates.io-index"
81 | checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
82 | dependencies = [
83 | "equivalent",
84 | "hashbrown",
85 | ]
86 |
87 | [[package]]
88 | name = "itoa"
89 | version = "1.0.11"
90 | source = "registry+https://github.com/rust-lang/crates.io-index"
91 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
92 |
93 | [[package]]
94 | name = "memchr"
95 | version = "2.7.4"
96 | source = "registry+https://github.com/rust-lang/crates.io-index"
97 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
98 |
99 | [[package]]
100 | name = "once_cell"
101 | version = "1.19.0"
102 | source = "registry+https://github.com/rust-lang/crates.io-index"
103 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
104 |
105 | [[package]]
106 | name = "only-peers"
107 | version = "0.1.0"
108 | dependencies = [
109 | "calimero-sdk",
110 | ]
111 |
112 | [[package]]
113 | name = "prettyplease"
114 | version = "0.2.22"
115 | source = "registry+https://github.com/rust-lang/crates.io-index"
116 | checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba"
117 | dependencies = [
118 | "proc-macro2",
119 | "syn",
120 | ]
121 |
122 | [[package]]
123 | name = "proc-macro-crate"
124 | version = "3.1.0"
125 | source = "registry+https://github.com/rust-lang/crates.io-index"
126 | checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
127 | dependencies = [
128 | "toml_edit",
129 | ]
130 |
131 | [[package]]
132 | name = "proc-macro-error"
133 | version = "1.0.4"
134 | source = "registry+https://github.com/rust-lang/crates.io-index"
135 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
136 | dependencies = [
137 | "proc-macro-error-attr",
138 | "proc-macro2",
139 | "quote",
140 | "version_check",
141 | ]
142 |
143 | [[package]]
144 | name = "proc-macro-error-attr"
145 | version = "1.0.4"
146 | source = "registry+https://github.com/rust-lang/crates.io-index"
147 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
148 | dependencies = [
149 | "proc-macro2",
150 | "quote",
151 | "version_check",
152 | ]
153 |
154 | [[package]]
155 | name = "proc-macro2"
156 | version = "1.0.86"
157 | source = "registry+https://github.com/rust-lang/crates.io-index"
158 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
159 | dependencies = [
160 | "unicode-ident",
161 | ]
162 |
163 | [[package]]
164 | name = "quote"
165 | version = "1.0.37"
166 | source = "registry+https://github.com/rust-lang/crates.io-index"
167 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
168 | dependencies = [
169 | "proc-macro2",
170 | ]
171 |
172 | [[package]]
173 | name = "ryu"
174 | version = "1.0.18"
175 | source = "registry+https://github.com/rust-lang/crates.io-index"
176 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
177 |
178 | [[package]]
179 | name = "serde"
180 | version = "1.0.209"
181 | source = "registry+https://github.com/rust-lang/crates.io-index"
182 | checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09"
183 | dependencies = [
184 | "serde_derive",
185 | ]
186 |
187 | [[package]]
188 | name = "serde_derive"
189 | version = "1.0.209"
190 | source = "registry+https://github.com/rust-lang/crates.io-index"
191 | checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
192 | dependencies = [
193 | "proc-macro2",
194 | "quote",
195 | "syn",
196 | ]
197 |
198 | [[package]]
199 | name = "serde_json"
200 | version = "1.0.127"
201 | source = "registry+https://github.com/rust-lang/crates.io-index"
202 | checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad"
203 | dependencies = [
204 | "itoa",
205 | "memchr",
206 | "ryu",
207 | "serde",
208 | ]
209 |
210 | [[package]]
211 | name = "syn"
212 | version = "2.0.76"
213 | source = "registry+https://github.com/rust-lang/crates.io-index"
214 | checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525"
215 | dependencies = [
216 | "proc-macro2",
217 | "quote",
218 | "unicode-ident",
219 | ]
220 |
221 | [[package]]
222 | name = "syn_derive"
223 | version = "0.1.8"
224 | source = "registry+https://github.com/rust-lang/crates.io-index"
225 | checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b"
226 | dependencies = [
227 | "proc-macro-error",
228 | "proc-macro2",
229 | "quote",
230 | "syn",
231 | ]
232 |
233 | [[package]]
234 | name = "thiserror"
235 | version = "1.0.63"
236 | source = "registry+https://github.com/rust-lang/crates.io-index"
237 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
238 | dependencies = [
239 | "thiserror-impl",
240 | ]
241 |
242 | [[package]]
243 | name = "thiserror-impl"
244 | version = "1.0.63"
245 | source = "registry+https://github.com/rust-lang/crates.io-index"
246 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
247 | dependencies = [
248 | "proc-macro2",
249 | "quote",
250 | "syn",
251 | ]
252 |
253 | [[package]]
254 | name = "toml_datetime"
255 | version = "0.6.8"
256 | source = "registry+https://github.com/rust-lang/crates.io-index"
257 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
258 |
259 | [[package]]
260 | name = "toml_edit"
261 | version = "0.21.1"
262 | source = "registry+https://github.com/rust-lang/crates.io-index"
263 | checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
264 | dependencies = [
265 | "indexmap",
266 | "toml_datetime",
267 | "winnow",
268 | ]
269 |
270 | [[package]]
271 | name = "unicode-ident"
272 | version = "1.0.12"
273 | source = "registry+https://github.com/rust-lang/crates.io-index"
274 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
275 |
276 | [[package]]
277 | name = "version_check"
278 | version = "0.9.5"
279 | source = "registry+https://github.com/rust-lang/crates.io-index"
280 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
281 |
282 | [[package]]
283 | name = "winnow"
284 | version = "0.5.40"
285 | source = "registry+https://github.com/rust-lang/crates.io-index"
286 | checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
287 | dependencies = [
288 | "memchr",
289 | ]
290 |
--------------------------------------------------------------------------------