├── 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 | 26 | 35 |
36 | 37 | 38 |
39 |
40 | 49 | 50 |
51 |
52 |
57 |
58 | 62 | {t.dialogTitle} 63 | 64 |
65 |

{props.error}

66 |
67 |
68 |
69 |
70 |
77 |
78 |
85 |
86 |
87 |
88 |
89 |
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 | 30 | 39 |
40 | 41 |
42 |
43 | 52 | 53 | {loading ? ( 54 |
55 |
56 | {t.loadingText} 57 |
58 | 59 |
60 | ) : ( 61 | <> 62 |
63 |
64 | 68 | {t.dialogTitle} 69 | 70 | 71 | 74 |
75 | 82 |
83 | {text.length}/250 84 |
85 |
86 |
87 |
88 |
89 |
107 | 108 | )} 109 |
110 |
111 |
112 |
113 |
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 | 30 | 39 |
40 | 41 |
42 |
43 | 52 | 53 | {loading ? ( 54 |
55 |
56 | {t.loadingText} 57 |
58 | 59 |
60 | ) : ( 61 | <> 62 |
63 |
64 | 68 | {t.dialogTitle} 69 | 70 | 73 | setTitle(e.target.value)} 79 | /> 80 | 83 |
84 | 91 |
92 | {content.length} 93 | {t.maxCharLength} 94 |
95 |
96 |
97 |
98 |
99 |
123 | 124 | )} 125 |
126 |
127 |
128 |
129 |
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 | --------------------------------------------------------------------------------