├── .eslintrc.json ├── public ├── favicon.png └── fonts │ └── Inter.ttf ├── jsconfig.json ├── postcss.config.js ├── next.config.js ├── pages ├── api │ └── hello.js ├── _document.js ├── _app.js ├── edit │ └── [post_id].js ├── post │ └── [post_id].js ├── create.jsx └── index.jsx ├── components ├── Badge.jsx ├── Hero.jsx ├── Footer.jsx ├── PostItem.jsx ├── Sidebar.jsx ├── Icons.jsx ├── Editor.js ├── ArticleContent.jsx └── Header.jsx ├── .gitignore ├── package.json ├── hooks └── useOutsideClick.js ├── utils └── index.js ├── tailwind.config.js ├── README.md └── styles └── globals.css /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OrbisWeb3/forum-template/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/fonts/Inter.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OrbisWeb3/forum-template/HEAD/public/fonts/Inter.ttf -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./*"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default function handler(req, res) { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /components/Badge.jsx: -------------------------------------------------------------------------------- 1 | export default function Badge({title, color}) { 2 | return( 3 |
{title}
4 | ) 5 | } 6 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .env 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | .pnpm-debug.log* 28 | 29 | # local env files 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "community-next", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@orbisclub/components": "^0.2.6", 13 | "@tailwindcss/forms": "^0.5.3", 14 | "@tailwindcss/typography": "^0.5.9", 15 | "eslint": "8.36.0", 16 | "eslint-config-next": "13.2.4", 17 | "html-react-parser": "^3.0.12", 18 | "marked": "^4.2.12", 19 | "next": "13.2.4", 20 | "react": "18.2.0", 21 | "react-dom": "18.2.0", 22 | "react-syntax-highlighter": "^15.5.0", 23 | "react-textarea-autosize": "^8.4.0", 24 | "react-time-ago": "^7.2.1" 25 | }, 26 | "devDependencies": { 27 | "autoprefixer": "^10.4.14", 28 | "postcss": "^8.4.21", 29 | "tailwindcss": "^3.2.7" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css' 2 | import { Orbis, OrbisProvider } from "@orbisclub/components"; 3 | import "@orbisclub/components/dist/index.modern.css"; 4 | 5 | /** Set the global forum context here (you can create categories using the dashboard by clicking on "Create a sub-context" from your main forum context) */ 6 | global.orbis_context = "kjzl6cwe1jw147eabkq3k4z6ka604w0xksr5k9ildy1glfe1ebkcfmtu8k2d94j"; 7 | 8 | /** Set the global chat context here (the chat displayed when users click on the "Community Chat" button) */ 9 | global.orbis_chat_context = "kjzl6cwe1jw147040w6bj3nkvny3ax30q76ib5ytxo6298psrx1oawa3wmme2jx"; 10 | 11 | let orbis = new Orbis({ 12 | useLit: false, 13 | node: "https://node2.orbis.club", 14 | PINATA_GATEWAY: 'https://orbis.mypinata.cloud/ipfs/', 15 | PINATA_API_KEY: process.env.pinata_api_key, 16 | PINATA_SECRET_API_KEY: process.env.pinata_secret_api_key 17 | }); 18 | 19 | export default function App({ Component, pageProps }) { 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /components/Hero.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HeroIllustration } from "./Icons"; 3 | 4 | function Hero({ title, description, image }) { 5 | return ( 6 |
7 | {/* Bg */} 8 |
31 | ); 32 | } 33 | 34 | export default Hero; 35 | -------------------------------------------------------------------------------- /hooks/useOutsideClick.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext, useRef } from "react"; 2 | 3 | export default function useOutsideClick(ref, handler) { 4 | useEffect( 5 | () => { 6 | const listener = (event) => { 7 | // Do nothing if clicking ref's element or descendent elements 8 | if (!ref.current || ref.current.contains(event.target)) { 9 | return; 10 | } 11 | handler(event); 12 | }; 13 | if (typeof window !== "undefined") { 14 | document.addEventListener("mousedown", listener); 15 | document.addEventListener("touchstart", listener); 16 | } 17 | return () => { 18 | if (typeof window !== "undefined") { 19 | document.removeEventListener("mousedown", listener); 20 | document.removeEventListener("touchstart", listener); 21 | } 22 | }; 23 | }, 24 | // Add ref and handler to effect dependencies 25 | // It's worth noting that because passed in handler is a new ... 26 | // ... function on every render that will cause this effect ... 27 | // ... callback/cleanup to run every render. It's not a big deal ... 28 | // ... but to optimize you can wrap handler in useCallback before ... 29 | // ... passing it into this hook. 30 | [ref, handler] 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /utils/index.js: -------------------------------------------------------------------------------- 1 | /** Convert an address into a short address with only the first 7 + last 7 characters */ 2 | export function shortAddress(address, number = 5) { 3 | if(!address) { 4 | return "-"; 5 | } 6 | 7 | const firstChars = address.substring(0, number); 8 | const lastChars = address.substr(address.length - number); 9 | return firstChars.concat('-', lastChars); 10 | } 11 | 12 | /** Wait for x ms in an async function */ 13 | export const sleep = (milliseconds) => { 14 | return new Promise(resolve => setTimeout(resolve, milliseconds)) 15 | } 16 | 17 | /** Returns current timestamp */ 18 | export function getTimestamp() { 19 | const cur_timestamp = Math.round(new Date().getTime() / 1000).toString() 20 | return cur_timestamp; 21 | } 22 | 23 | /** Will turn IPFS data into a readable URL */ 24 | export function getIpfsLink(media) { 25 | let _url = media.url; 26 | if(media.gateway) { 27 | _url = _url.replace("ipfs://", media.gateway) 28 | } else { 29 | _url = _url.replace("ipfs://", "https://orbis.mypinata.cloud/ipfs/") 30 | } 31 | 32 | /** Revert old Adam's image */ 33 | if(_url == "https://ipfsgateway.orbis.club/ipfs/QmW6o4Phn7wJ3TLX8pqgqGQVUTDoauCF1DozXPM9HDQVZ8") { 34 | _url = "https://orbis.mypinata.cloud/ipfs/Qmd5tZUHAKUpWFtGj7HnTXWVdKGVXxzSxmLBXEq3eXhMT1"; 35 | } 36 | 37 | return _url; 38 | } 39 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./app/**/*.{js,ts,jsx,tsx}", 5 | "./pages/**/*.{js,ts,jsx,tsx}", 6 | "./components/**/*.{js,ts,jsx,tsx}", 7 | 8 | // Or if using `src` directory: 9 | "./src/**/*.{js,ts,jsx,tsx}", 10 | ], 11 | theme: { 12 | extend: { 13 | fontSize: { 14 | xs: ['0.75rem', { lineHeight: '1.5' }], 15 | sm: ['0.875rem', { lineHeight: '1.5715' }], 16 | base: ['1rem', { lineHeight: '1.5', letterSpacing: '-0.01em' }], 17 | lg: ['1.125rem', { lineHeight: '1.5', letterSpacing: '-0.01em' }], 18 | xl: ['1.25rem', { lineHeight: '1.5', letterSpacing: '-0.01em' }], 19 | '2xl': ['1.5rem', { lineHeight: '1.415', letterSpacing: '-0.01em' }], 20 | '3xl': ['1.875rem', { lineHeight: '1.333', letterSpacing: '-0.01em' }], 21 | '4xl': ['2.25rem', { lineHeight: '1.277', letterSpacing: '-0.01em' }], 22 | '5xl': ['3rem', { lineHeight: '1', letterSpacing: '-0.01em' }], 23 | '6xl': ['3.75rem', { lineHeight: '1', letterSpacing: '-0.01em' }], 24 | '7xl': ['4.5rem', { lineHeight: '1', letterSpacing: '-0.01em' }], 25 | }, 26 | letterSpacing: { 27 | tighter: '-0.02em', 28 | tight: '-0.01em', 29 | normal: '0', 30 | wide: '0.01em', 31 | wider: '0.02em', 32 | widest: '0.4em', 33 | }, 34 | }, 35 | }, 36 | plugins: [ 37 | // eslint-disable-next-line global-require 38 | require('@tailwindcss/forms'), 39 | require('@tailwindcss/typography') 40 | ], 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 18 | 19 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 20 | 21 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 22 | 23 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 24 | 25 | ## Learn More 26 | 27 | To learn more about Next.js, take a look at the following resources: 28 | 29 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 30 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 31 | 32 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 33 | 34 | ## Deploy on Vercel 35 | 36 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 37 | 38 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 39 | -------------------------------------------------------------------------------- /pages/edit/[post_id].js: -------------------------------------------------------------------------------- 1 | import Editor from "../../components/Editor"; 2 | import Header from '../../components/Header'; 3 | import Hero from '../../components/Hero'; 4 | import Sidebar from '../../components/Sidebar'; 5 | import Footer from '../../components/Footer'; 6 | import { Orbis, useOrbis, User } from "@orbisclub/components"; 7 | 8 | export default function Edit({post}) { 9 | const { orbis, user, setConnectModalVis } = useOrbis(); 10 | 11 | return ( 12 |
13 |
14 |
15 | 16 | {/* Page content */} 17 |
18 | {/* Site header */} 19 |
20 | 21 | 22 | {/* Page content */} 23 |
24 |
25 | 26 |
27 | 28 | {/* Show post editor or connect button */} 29 |
30 | {user ? 31 | 32 | : 33 |
34 |

You must be connected to share a post in this forum.

35 | 36 |
37 | } 38 |
39 | 40 |
41 |
42 |
43 |
44 |
45 |
46 | 47 | {/* Site footer */} 48 |
49 |
50 | ); 51 | } 52 | 53 | /** Load content for Blog */ 54 | Edit.getInitialProps = async (context) => { 55 | let orbis_server = new Orbis({ 56 | useLit: false 57 | }); 58 | let { data, error } = await orbis_server.getPost(context.query.post_id); 59 | /** Return results */ 60 | return { 61 | post_id: context.query.post_id, 62 | post: data ? data : null 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /pages/post/[post_id].js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | import Head from 'next/head'; 4 | import Header from '../../components/Header'; 5 | import ArticleContent from '../../components/ArticleContent'; 6 | import Sidebar from '../../components/Sidebar'; 7 | import Footer from '../../components/Footer'; 8 | import { Orbis, Comments, User, useOrbis } from "@orbisclub/components"; 9 | 10 | export default function Post({ post, post_id }) { 11 | const { orbis, user } = useOrbis(); 12 | return ( 13 | <> 14 | 15 | {/** Title */} 16 | {post.content?.title} 17 | 18 | 19 | {/** Description */} 20 | 21 | 22 | 23 | 24 | {post.content?.media && post.content?.media.length > 0 && 25 | <> 26 | {/***/} 27 | 28 | 29 | 30 | } 31 | 32 |
33 |
34 |
35 |
36 |
37 |
38 | {/* Page content*/} 39 | 40 | 41 |
42 |
43 |
44 |
45 | {/* Site footer*/} 46 |
47 |
48 | 49 | ); 50 | } 51 | 52 | /** Load content for Blog */ 53 | Post.getInitialProps = async (context) => { 54 | let orbis_server = new Orbis({ 55 | useLit: false 56 | }); 57 | let { data, error } = await orbis_server.getPost(context.query.post_id); 58 | /** Return results */ 59 | return { 60 | post_id: context.query.post_id, 61 | post: data ? data : null 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | import { Logo, TwitterIcon, GithubIcon } from "./Icons"; 4 | 5 | function Footer() { 6 | return ( 7 |
8 |
9 |
10 | {/* Top area */} 11 |
12 |
13 | {/* Logo */} 14 | 15 | 16 | 17 |
18 | {/* Right links */} 19 |
20 |
    21 |
  • 22 | 23 | Learn more 24 | 25 |
  • 26 |
  • 27 | 28 | Go to useorbis.com 29 | 30 |
  • 31 | 32 |
33 |
34 |
35 | 36 | {/* Bottom area */} 37 |
38 | {/* Social links */} 39 |
    40 |
  • 41 | 45 | 46 | 47 |
  • 48 |
  • 49 | 53 | 54 | 55 |
  • 56 |
57 | 58 | {/* Copyright */} 59 |
Copyright © Orbis Labs. All rights reserved.
60 |
61 |
62 |
63 |
64 | ); 65 | } 66 | 67 | export default Footer; 68 | -------------------------------------------------------------------------------- /pages/create.jsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import Editor from "../components/Editor"; 3 | import Header from '../components/Header'; 4 | import Hero from '../components/Hero'; 5 | import Sidebar from '../components/Sidebar'; 6 | import Footer from '../components/Footer'; 7 | import { useOrbis, User } from "@orbisclub/components"; 8 | 9 | export default function Create() { 10 | const { orbis, user, setConnectModalVis } = useOrbis(); 11 | 12 | return ( 13 | <> 14 | 15 | {/** Title */} 16 | Share a new post | Orbis Forum 17 | 18 | 19 | {/** Description */} 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 | 28 | {/* Page content */} 29 |
30 | {/* Site header */} 31 |
32 | 33 | 34 | {/* Page content */} 35 |
36 |
37 | 38 |
39 | 40 | {/* Show post editor or connect button */} 41 |
42 | {user ? 43 | 44 | : 45 |
46 |

You must be connected to share a post in this forum.

47 | 48 |
49 | } 50 |
51 | 52 |
53 |
54 |
55 |
56 |
57 |
58 | 59 | {/* Site footer */} 60 |
61 |
62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /components/PostItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | import Badge from "./Badge"; 4 | import { User, UserBadge, useOrbis } from "@orbisclub/components"; 5 | import { shortAddress } from "../utils"; 6 | import { ExternalLinkIcon, CommentsIcon } from "./Icons"; 7 | 8 | export default function PostItem({post}) { 9 | return ( 10 |
11 |
12 |
13 |
14 |
15 |
16 |

17 | 18 | {post.content.title} 19 | 20 |

21 |

{post.content.body.substring(0,180)}..

22 |
23 |
24 | 25 | 26 | in 27 | {post.context_details?.context_details && 28 | 29 | } 30 | 31 | · 32 | 33 | {/** Show count replies if any */} 34 | {(post.count_replies && post.count_replies > 0) ? 35 | <> 36 | {post.count_replies} 37 | · 38 | 39 | : 40 | <> 41 | } 42 | 43 | {/** Proof link to Cerscan */} 44 | {post.stream_id && 45 | {shortAddress(post.stream_id, 12)} 46 | } 47 |
48 |
49 |
50 |
51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /components/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import Link from 'next/link'; 3 | import { useOrbis, User } from "@orbisclub/components"; 4 | import { LoadingCircle } from "./Icons"; 5 | import ReactTimeAgo from 'react-time-ago' 6 | 7 | function Sidebar() { 8 | return ( 9 | 26 | ); 27 | } 28 | 29 | /** Show recent discussions */ 30 | const RecentDiscussions = () => { 31 | const { orbis, user } = useOrbis(); 32 | const [loading, setLoading] = useState(false); 33 | const [posts, setPosts] = useState([]); 34 | 35 | /** Load all of the categories (sub-contexts) available in this forum */ 36 | useEffect(() => { 37 | loadPosts(global.orbis_context, true); 38 | async function loadPosts(context, include_child_contexts) { 39 | setLoading(true); 40 | let { data, error } = await orbis.getPosts({ 41 | context: context, 42 | only_master: true, 43 | include_child_contexts: include_child_contexts, 44 | order_by: 'last_reply_timestamp' 45 | }, 0, 5); 46 | setLoading(false); 47 | 48 | 49 | if(error) { 50 | console.log("error:", error); 51 | } 52 | if(data) { 53 | setPosts(data); 54 | } 55 | } 56 | }, []) 57 | 58 | return( 59 |
60 |
Active Discussions
61 |
    62 | {(posts && posts.length > 0) ? 63 | <> 64 | {posts.map((post, key) => { 65 | return( 66 |
  • 67 |

    68 | {post.content.title} 69 |

    70 |
    71 | Last activity 72 |
    73 |
  • 74 | ); 75 | })} 76 | 77 | : 78 |
    79 |

    There isn't any posts here yet.

    80 |
    81 | } 82 |
83 |
84 | ) 85 | } 86 | 87 | /** Will loop through all categories and display them */ 88 | const Categories = () => { 89 | const { orbis, user } = useOrbis(); 90 | const [loading, setLoading] = useState(false); 91 | const [categories, setCategories] = useState([]); 92 | 93 | /** Load all of the categories (sub-contexts) available in this forum */ 94 | useEffect(() => { 95 | loadContexts(); 96 | async function loadContexts() { 97 | setLoading(true); 98 | let { data, error } = await orbis.api.from("orbis_contexts").select().eq('context', global.orbis_context).order('created_at', { ascending: false }); 99 | setCategories(data); 100 | setLoading(false); 101 | } 102 | }, []); 103 | 104 | return( 105 |
106 |
Active Categories
107 | {loading ? 108 |
109 | 110 |
111 | : 112 |
    113 | {(categories && categories.length > 0) ? 114 | <> 115 | {categories.map((category, key) => { 116 | return ( 117 |
  • 118 |
    119 |
    120 |
    121 | {category.content.displayName} 122 |
    123 |
    124 |
  • 125 | ); 126 | })} 127 | 128 | : 129 |
    130 |

    There isn't any category in this forum.

    131 |
    132 | } 133 | 134 |
135 | } 136 | 137 |
138 | ); 139 | } 140 | 141 | const NewsletterBlock = () => { 142 | return( 143 |
144 |
145 | 166 |
167 | ) 168 | } 169 | 170 | export default Sidebar; 171 | -------------------------------------------------------------------------------- /pages/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import Link from 'next/link'; 3 | import Head from 'next/head'; 4 | import Header from '../components/Header'; 5 | import Hero from '../components/Hero'; 6 | import Sidebar from '../components/Sidebar'; 7 | import PostItem from '../components/PostItem'; 8 | import Footer from '../components/Footer'; 9 | import { HeroOrbisIcon , LoadingCircle } from "../components/Icons"; 10 | import { Orbis, useOrbis } from "@orbisclub/components"; 11 | 12 | function Home({defaultPosts}) { 13 | const { orbis, user } = useOrbis(); 14 | const [nav, setNav] = useState("all"); 15 | const [page, setPage] = useState(0); 16 | const [posts, setPosts] = useState(defaultPosts); 17 | const [loading, setLoading] = useState(false); 18 | const [categories, setCategories] = useState([]); 19 | 20 | /** Load all of the categories (sub-contexts) available in this forum */ 21 | useEffect(() => { 22 | loadContexts(); 23 | 24 | /** Load all categories / contexts under the global forum context */ 25 | async function loadContexts() { 26 | let { data, error } = await orbis.api.from("orbis_contexts").select().eq('context', global.orbis_context).order('created_at', { ascending: false }); 27 | setCategories(data); 28 | } 29 | }, []); 30 | 31 | /** Will re-load list of posts when navigation is updated */ 32 | useEffect(() => { 33 | /** Reset page */ 34 | setPage(0); 35 | 36 | /** Load posts */ 37 | if(nav == "all") { 38 | loadPosts(global.orbis_context, true, 0); 39 | } else { 40 | loadPosts(nav, true, 0); 41 | } 42 | }, [nav]); 43 | 44 | /** Will re-load the posts when page is updated */ 45 | useEffect(() => { 46 | if(nav == "all") { 47 | loadPosts(global.orbis_context, true, page); 48 | } else { 49 | loadPosts(nav, true, page); 50 | } 51 | }, [page]); 52 | 53 | /** Load list of posts using the Orbis SDK */ 54 | async function loadPosts(context, include_child_contexts, _page) { 55 | setLoading(true); 56 | let { data, error } = await orbis.getPosts({ 57 | context: context, 58 | only_master: true, 59 | include_child_contexts: include_child_contexts, 60 | }, _page, 25); 61 | 62 | /** Save data in posts state */ 63 | if(data) { 64 | setPosts(data); 65 | } 66 | 67 | /** Disable loading state */ 68 | setLoading(false); 69 | } 70 | 71 | return ( 72 | <> 73 | 74 | {/** Title */} 75 | Orbis Forum | Let's build web3 social together 76 | 77 | 78 | {/** Description */} 79 | 80 | 81 | 82 | 83 |
84 |
85 |
86 | 87 | {/* Page content */} 88 |
89 | {/* Site header */} 90 |
91 | 92 | {/* Page sections */} 93 | } /> 94 | 95 | {/* Page content */} 96 |
97 |
98 | 99 |
100 | 101 | {/* Main content */} 102 |
103 |
104 | 105 | {/** Show loading state or list of posts */} 106 | {loading ? 107 |
108 | 109 |
110 | : 111 | <> 112 | {/* Display posts if any */} 113 | {(posts && posts.length > 0) ? 114 | <> 115 |
116 |
117 | {posts.map((post) => { 118 | return ( 119 | 120 | ); 121 | })} 122 |
123 | 124 | {/* Handle pagination */} 125 | {posts && posts.length >= 25 && 126 |
127 | 130 |
131 | } 132 | 133 |
134 | 135 | : 136 |
137 |

There aren't any posts shared here.

138 |
139 | } 140 | 141 | } 142 |
143 |
144 | 145 |
146 |
147 |
148 |
149 |
150 |
151 | 152 | {/* Site footer */} 153 |
154 |
155 | 156 | ); 157 | } 158 | 159 | const CategoriesNavigation = ({ categories, nav, setNav }) => { 160 | return( 161 |
162 |
163 | {/* Right: Button */} 164 |
165 | Create Post 166 |
167 | 168 | {/* Left: Links */} 169 |
    170 | 171 | {categories.map((category, key) => { 172 | return ( 173 | 174 | ); 175 | })} 176 |
177 |
178 |
179 | ) 180 | } 181 | 182 | const NavItem = ({selected, category, onClick}) => { 183 | return( 184 |
  • 185 | onClick(category.stream_id)}>{category.content.displayName} 186 |
  • 187 | ) 188 | } 189 | 190 | /** Load blog articles */ 191 | Home.getInitialProps = async (context) => { 192 | let orbis_server = new Orbis({ 193 | useLit: false 194 | }); 195 | let { data, error } = await orbis_server.getPosts({ 196 | context: global.orbis_context, 197 | only_master: true, 198 | include_child_contexts: true 199 | }); 200 | 201 | /** Return results */ 202 | return { 203 | defaultPosts: data 204 | }; 205 | } 206 | 207 | export default Home; 208 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @font-face { 6 | font-family: "Inter"; 7 | src: url("/fonts/Inter.ttf"); 8 | } 9 | 10 | html { 11 | /** Colors */ 12 | --brand-color: #4E75F6; 13 | --brand-color-400: #698af4; 14 | --brand-color-400-reverse: #FFF; 15 | --brand-color-reverse: #FFF; 16 | --brand-color-hover: #355EE7; 17 | --primary-color: #333; 18 | --secondary-color: #404c5c; 19 | --tertiary-color: #94a3b8; 20 | --border-color-primary: #e2e8f0; 21 | /***/ 22 | 23 | } 24 | body { 25 | background: #FFF; 26 | /*background-image: linear-gradient(to right, var(--tw-gradient-stops)); 27 | --tw-gradient-from: #FEF6EB; 28 | --tw-gradient-to: rgb(254 246 235 / 0); 29 | --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); 30 | --tw-gradient-to: #FFF;*/ 31 | } 32 | p, ul, li, span, input, button { 33 | } 34 | 35 | .bg-blur { 36 | -webkit-backdrop-filter: blur(1px); 37 | backdrop-filter: blur(1px); 38 | } 39 | 40 | /** Theme colors */ 41 | .bg-main, .bg-brand { 42 | background-color: var(--brand-color); 43 | } 44 | .bg-brand-400 { 45 | background-color: var(--brand-color-400); 46 | } 47 | .bg-main-hover:hover, .bg-brand-hover:hover { 48 | background-color: var(--brand-color-hover); 49 | } 50 | .border-brand { 51 | border-color: var(--brand-color); 52 | } 53 | .text-main, .text-brand { 54 | color: var(--brand-color); 55 | } 56 | .text-brand-hover:hover { 57 | color: var(--brand-color-hover); 58 | } 59 | .text-primary { 60 | color: var(--primary-color); 61 | } 62 | .text-secondary { 63 | color: var(--secondary-color); 64 | } 65 | .text-tertiary { 66 | color: var(--tertiary-color); 67 | } 68 | .text-main-hover:hover { 69 | color: var(--brand-color-hover); 70 | } 71 | .btn-main, .btn-brand { 72 | cursor: pointer; 73 | background: var(--brand-color); 74 | color: var(--brand-color-reverse); 75 | } 76 | .btn-main:hover, .btn-brand:hover { 77 | background-color: var(--brand-color-hover); 78 | } 79 | .btn-secondary { 80 | cursor: pointer; 81 | background: var(--brand-color-reverse); 82 | color: var(--brand-color); 83 | border: 1px solid var(--brand-color); 84 | } 85 | .btn-secondary:hover { 86 | color: var(--brand-color-hover); 87 | border: 1px solid var(--brand-color-hover); 88 | } 89 | .btn-brand-400 { 90 | cursor: pointer; 91 | background: var(--brand-color-400); 92 | color: var(--brand-color-400-reverse); 93 | } 94 | .btn-brand-400:hover { 95 | background: var(--brand-color); 96 | } 97 | 98 | /** Post details style */ 99 | 100 | .article-content p, .article-content ul, .article-content li { 101 | color: var(--secondary-color); 102 | } 103 | .article-content.text-white p { 104 | color: #FFF !important; 105 | } 106 | .article-content a { 107 | color: #08c; 108 | } 109 | .article-content a:hover { 110 | text-decoration: underline; 111 | } 112 | .article-content h2 { 113 | --tw-text-opacity: 1; 114 | color: var(--primary-color); 115 | font-family: Aspekta, sans-serif; 116 | font-size: 1.875rem; 117 | line-height: 1.333; 118 | letter-spacing: -0.01em; 119 | font-weight: 650; 120 | } 121 | .article-content pre { 122 | background: #1e293b; 123 | border-radius: 4px; 124 | padding: 0.75rem; 125 | font-size: 13px; 126 | line-height: 1.45; 127 | margin-top: 10px; 128 | margin-bottom: 10px; 129 | } 130 | .article-content pre code { 131 | color: #FFF; 132 | font-size: 13px; 133 | white-space: pre-wrap; 134 | } 135 | code, code > * { 136 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important; 137 | white-space: pre-wrap !important; 138 | } 139 | .article-content > code, .article-content > p > code, .article-content > ul > li > p > code, .article-content > ul > li > code { 140 | color: #000; 141 | font-size: 13px; 142 | background: #e2e8f0; 143 | padding: 0.25rem; 144 | border-radius: 4px; 145 | } 146 | .article-content > ul > li { 147 | list-style-type: disc; 148 | margin-left: 15px; 149 | line-height: 1.65; 150 | margin-bottom: 5px; 151 | } 152 | .article-content table { 153 | border-spacing: 0 15px; 154 | } 155 | .article-content thead { 156 | border-bottom: 2px solid var(--border-color-primary); 157 | color: var(--primary-color); 158 | text-align: left; 159 | } 160 | .article-content tbody tr { 161 | border-bottom: 1px solid var(--border-color-primary); 162 | } 163 | .article-content td, .article-content th { 164 | padding-top: 5px; 165 | padding-bottom: 5px; 166 | padding-right: 5px; 167 | } 168 | 169 | /** Minor utilities */ 170 | .min-height-200 { 171 | min-height: 200px !important; 172 | } 173 | 174 | :root { 175 | --max-width: 1100px; 176 | --border-radius: 12px; 177 | --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 178 | 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 179 | 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; 180 | 181 | --foreground-rgb: 0, 0, 0; 182 | --background-start-rgb: 214, 219, 220; 183 | --background-end-rgb: 255, 255, 255; 184 | 185 | --primary-glow: conic-gradient( 186 | from 180deg at 50% 50%, 187 | #16abff33 0deg, 188 | #0885ff33 55deg, 189 | #54d6ff33 120deg, 190 | #0071ff33 160deg, 191 | transparent 360deg 192 | ); 193 | --secondary-glow: radial-gradient( 194 | rgba(255, 255, 255, 1), 195 | rgba(255, 255, 255, 0) 196 | ); 197 | 198 | --tile-start-rgb: 239, 245, 249; 199 | --tile-end-rgb: 228, 232, 233; 200 | --tile-border: conic-gradient( 201 | #00000080, 202 | #00000040, 203 | #00000030, 204 | #00000020, 205 | #00000010, 206 | #00000010, 207 | #00000080 208 | ); 209 | 210 | --callout-rgb: 238, 240, 241; 211 | --callout-border-rgb: 172, 175, 176; 212 | --card-rgb: 180, 185, 188; 213 | --card-border-rgb: 131, 134, 135; 214 | } 215 | 216 | @media (prefers-color-scheme: dark) { 217 | :root { 218 | --foreground-rgb: 255, 255, 255; 219 | --background-start-rgb: 0, 0, 0; 220 | --background-end-rgb: 0, 0, 0; 221 | 222 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); 223 | --secondary-glow: linear-gradient( 224 | to bottom right, 225 | rgba(1, 65, 255, 0), 226 | rgba(1, 65, 255, 0), 227 | rgba(1, 65, 255, 0.3) 228 | ); 229 | 230 | --tile-start-rgb: 2, 13, 46; 231 | --tile-end-rgb: 2, 5, 19; 232 | --tile-border: conic-gradient( 233 | #ffffff80, 234 | #ffffff40, 235 | #ffffff30, 236 | #ffffff20, 237 | #ffffff10, 238 | #ffffff10, 239 | #ffffff80 240 | ); 241 | 242 | --callout-rgb: 20, 20, 20; 243 | --callout-border-rgb: 108, 108, 108; 244 | --card-rgb: 100, 100, 100; 245 | --card-border-rgb: 200, 200, 200; 246 | } 247 | } 248 | 249 | * { 250 | box-sizing: border-box; 251 | padding: 0; 252 | margin: 0; 253 | } 254 | 255 | html, 256 | body { 257 | max-width: 100vw; 258 | overflow-x: hidden; 259 | } 260 | 261 | a { 262 | color: inherit; 263 | text-decoration: none; 264 | } 265 | 266 | @media (prefers-color-scheme: dark) { 267 | html { 268 | color-scheme: dark; 269 | } 270 | } 271 | 272 | /** Default styles */ 273 | /* Typography */ 274 | .h1 { 275 | @apply text-5xl font-bold; 276 | } 277 | 278 | .h2 { 279 | @apply text-2xl font-bold; 280 | } 281 | 282 | .h3 { 283 | @apply text-3xl font-bold; 284 | } 285 | 286 | .h4 { 287 | @apply text-2xl font-bold; 288 | } 289 | 290 | @screen md { 291 | .h1 { 292 | @apply text-6xl; 293 | } 294 | 295 | .h2 { 296 | @apply text-5xl; 297 | } 298 | } 299 | 300 | /* Buttons */ 301 | .btn, 302 | .btn-sm { 303 | @apply text-sm font-medium inline-flex items-center justify-center rounded-full leading-5 whitespace-nowrap transition duration-150 ease-in-out; 304 | } 305 | 306 | .btn { 307 | @apply px-5 py-2.5; 308 | } 309 | 310 | .btn-sm { 311 | @apply px-4 py-2; 312 | } 313 | 314 | /* Forms */ 315 | input[type="search"]::-webkit-search-decoration, 316 | input[type="search"]::-webkit-search-cancel-button, 317 | input[type="search"]::-webkit-search-results-button, 318 | input[type="search"]::-webkit-search-results-decoration { 319 | -webkit-appearance: none; 320 | } 321 | 322 | .form-input, 323 | .form-textarea, 324 | .form-multiselect, 325 | .form-select, 326 | .form-checkbox, 327 | .form-radio { 328 | @apply bg-transparent bg-gradient-to-tr from-slate-800/20 via-slate-800/50 to-slate-800/20 border border-slate-700 focus:bg-slate-800 focus:border-slate-600; 329 | } 330 | 331 | .form-input, 332 | .form-textarea, 333 | .form-multiselect, 334 | .form-select, 335 | .form-checkbox { 336 | @apply rounded; 337 | } 338 | 339 | .form-input, 340 | .form-textarea, 341 | .form-multiselect, 342 | .form-select { 343 | @apply text-sm px-3 py-1.5; 344 | } 345 | 346 | .form-input, 347 | .form-textarea { 348 | @apply placeholder-slate-600; 349 | } 350 | 351 | .form-select { 352 | @apply pr-10; 353 | } 354 | 355 | .form-checkbox, 356 | .form-radio { 357 | @apply text-indigo-500 border-2 rounded-sm; 358 | } 359 | 360 | /* Chrome, Safari and Opera */ 361 | .no-scrollbar::-webkit-scrollbar { 362 | display: none; 363 | } 364 | 365 | .no-scrollbar { 366 | -ms-overflow-style: none; 367 | /* IE and Edge */ 368 | scrollbar-width: none; 369 | /* Firefox */ 370 | } 371 | 372 | 373 | /** Range slider style */ 374 | :root { 375 | --range-thumb-size: 36px; 376 | } 377 | 378 | input[type=range] { 379 | appearance: none; 380 | background: #ccc; 381 | border-radius: 3px; 382 | height: 6px; 383 | margin-top: (--range-thumb-size - 6px) * 0.5; 384 | margin-bottom: (--range-thumb-size - 6px) * 0.5; 385 | --thumb-size: #{--range-thumb-size}; 386 | } 387 | 388 | input[type=range]::-webkit-slider-thumb { 389 | appearance: none; 390 | -webkit-appearance: none; 391 | background-color: #000; 392 | background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 .5v7L12 4zM0 4l4 3.5v-7z' fill='%23FFF' fill-rule='nonzero'/%3E%3C/svg%3E"); 393 | background-position: center; 394 | background-repeat: no-repeat; 395 | border: 0; 396 | border-radius: 50%; 397 | cursor: pointer; 398 | height: --range-thumb-size; 399 | width: --range-thumb-size; 400 | } 401 | 402 | input[type=range]::-moz-range-thumb { 403 | background-color: #000; 404 | background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 .5v7L12 4zM0 4l4 3.5v-7z' fill='%23FFF' fill-rule='nonzero'/%3E%3C/svg%3E"); 405 | background-position: center; 406 | background-repeat: no-repeat; 407 | border: 0; 408 | border: none; 409 | border-radius: 50%; 410 | cursor: pointer; 411 | height: --range-thumb-size; 412 | width: --range-thumb-size; 413 | } 414 | 415 | input[type=range]::-ms-thumb { 416 | background-color: #000; 417 | background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 .5v7L12 4zM0 4l4 3.5v-7z' fill='%23FFF' fill-rule='nonzero'/%3E%3C/svg%3E"); 418 | background-position: center; 419 | background-repeat: no-repeat; 420 | border: 0; 421 | border-radius: 50%; 422 | cursor: pointer; 423 | height: --range-thumb-size; 424 | width: --range-thumb-size; 425 | } 426 | 427 | input[type=range]::-moz-focus-outer { 428 | border: 0; 429 | } 430 | 431 | /** Toggle switch style */ 432 | /* Switch element */ 433 | .form-switch { 434 | @apply relative select-none; 435 | width: 44px; 436 | } 437 | 438 | .form-switch label { 439 | @apply block overflow-hidden cursor-pointer h-6 rounded-full; 440 | } 441 | 442 | .form-switch label>span:first-child { 443 | @apply absolute block rounded-full; 444 | width: 20px; 445 | height: 20px; 446 | top: 2px; 447 | left: 2px; 448 | right: 50%; 449 | transition: all .15s ease-out; 450 | } 451 | 452 | .form-switch input[type="checkbox"]:checked+label { 453 | @apply bg-indigo-500; 454 | } 455 | 456 | .form-switch input[type="checkbox"]:checked+label>span:first-child { 457 | left: 22px; 458 | } 459 | 460 | .form-switch input[type="checkbox"]:disabled+label { 461 | @apply cursor-not-allowed bg-slate-100 border border-slate-200; 462 | } 463 | 464 | .form-switch input[type="checkbox"]:disabled+label>span:first-child { 465 | @apply bg-slate-400; 466 | } 467 | 468 | /** Additional theme styles */ 469 | .form-input, 470 | .form-textarea, 471 | .form-multiselect, 472 | .form-select, 473 | .form-checkbox, 474 | .form-radio { 475 | @apply focus:ring-0; 476 | } 477 | 478 | /* Hamburger button */ 479 | .hamburger svg>*:nth-child(1), 480 | .hamburger svg>*:nth-child(2), 481 | .hamburger svg>*:nth-child(3) { 482 | transform-origin: center; 483 | transform: rotate(0deg); 484 | } 485 | 486 | .hamburger svg>*:nth-child(1) { 487 | transition: y 0.1s 0.25s ease-in, transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19), opacity 0.1s ease-in; 488 | } 489 | 490 | .hamburger svg>*:nth-child(2) { 491 | transition: transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19); 492 | } 493 | 494 | .hamburger svg>*:nth-child(3) { 495 | transition: y 0.1s 0.25s ease-in, transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19), width 0.1s 0.25s ease-in; 496 | } 497 | 498 | .hamburger.active svg>*:nth-child(1) { 499 | opacity: 0; 500 | y: 11; 501 | transform: rotate(225deg); 502 | transition: y 0.1s ease-out, transform 0.22s 0.12s cubic-bezier(0.215, 0.61, 0.355, 1), opacity 0.1s 0.12s ease-out; 503 | } 504 | 505 | .hamburger.active svg>*:nth-child(2) { 506 | transform: rotate(225deg); 507 | transition: transform 0.22s 0.12s cubic-bezier(0.215, 0.61, 0.355, 1); 508 | } 509 | 510 | .hamburger.active svg>*:nth-child(3) { 511 | y: 11; 512 | transform: rotate(135deg); 513 | transition: y 0.1s ease-out, transform 0.22s 0.12s cubic-bezier(0.215, 0.61, 0.355, 1), width 0.1s ease-out; 514 | } 515 | -------------------------------------------------------------------------------- /components/Icons.jsx: -------------------------------------------------------------------------------- 1 | export const MenuVerticalIcon = () => { 2 | return( 3 | 4 | 5 | 6 | 7 | 8 | ) 9 | } 10 | 11 | export const TwitterIcon = () => { 12 | return( 13 | 14 | 15 | 16 | ) 17 | } 18 | 19 | export const CommentsIcon = ({style}) => { 20 | return( 21 | 22 | 23 | 24 | ) 25 | } 26 | 27 | export const GithubIcon = () => { 28 | return( 29 | 30 | 31 | 32 | ) 33 | } 34 | 35 | export const Logo = () => { 36 | return( 37 | 38 | 39 | 40 | 41 | ) 42 | } 43 | 44 | export const MakerLogo = () => { 45 | return( 46 | 47 | ) 48 | } 49 | 50 | export const HeroIllustration = () => { 51 | return( 52 | 53 | 54 | 55 | ) 56 | } 57 | 58 | export const EmptyImg = ({className}) => { 59 | return( 60 | 61 | 62 | 63 | 64 | ) 65 | } 66 | 67 | export const LinkIcon = ({style}) => { 68 | return( 69 | 70 | 71 | 72 | 73 | ) 74 | } 75 | 76 | export const BackIcon = () => { 77 | return( 78 | 79 | 80 | 81 | ) 82 | } 83 | 84 | export const HomeIcon = () => { 85 | return( 86 | 87 | 88 | 89 | ) 90 | } 91 | 92 | export const CommunityIcon = ({style}) => { 93 | return( 94 | 95 | 96 | 97 | ) 98 | } 99 | 100 | export const PanelRight = ({style}) => { 101 | return( 102 | 103 | 104 | 105 | 106 | ) 107 | } 108 | 109 | export const ExternalLinkIcon = ({style}) => { 110 | return( 111 | 112 | 113 | 114 | 115 | ) 116 | } 117 | export const LoadingCircle = () => { 118 | return( 119 | 120 | 121 | 122 | 123 | ) 124 | } 125 | 126 | export const SearchIcon = () => { 127 | return( 128 | 129 | 132 | 133 | ) 134 | } 135 | 136 | export const HeroOrbisIcon = () => { 137 | return( 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | ) 159 | } 160 | 161 | export const HeroDaiIcon = () => { 162 | return( 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | ) 183 | } 184 | 185 | export const EditIcon = ({style}) => { 186 | return( 187 | 188 | 189 | 190 | 191 | ) 192 | } 193 | 194 | export const CodeIcon = ({style}) => { 195 | return( 196 | 197 | 198 | 199 | 200 | ) 201 | } 202 | -------------------------------------------------------------------------------- /components/Editor.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | import TextareaAutosize from 'react-textarea-autosize'; 3 | import { useOrbis, User, AccessRulesModal, checkContextAccess } from "@orbisclub/components"; 4 | import ReactTimeAgo from 'react-time-ago' 5 | import Link from 'next/link'; 6 | import { shortAddress, getIpfsLink, getTimestamp, sleep } from "../utils"; 7 | import { useRouter } from 'next/router'; 8 | import { ExternalLinkIcon, LinkIcon, CodeIcon, LoadingCircle } from "./Icons"; 9 | import ArticleContent from "./ArticleContent"; 10 | 11 | const Editor = ({post}) => { 12 | const { orbis, user, credentials } = useOrbis(); 13 | const router = useRouter(); 14 | const [title, setTitle] = useState(post?.content?.title ? post.content.title : ""); 15 | const [body, setBody] = useState(post?.content?.body ? post.content.body : ""); 16 | const [media, setMedia] = useState(post?.content?.media ? post.content.media : []); 17 | const [category, setCategory] = useState(post?.content?.context ? post.content.context : ""); 18 | const [categoryAccessRules, setCategoryAccessRules] = useState([]); 19 | const [accessRulesLoading, setAccessRulesLoading] = useState(false); 20 | const [hasAccess, setHasAccess] = useState(false); 21 | const [accessRulesModalVis, setAccessRulesModalVis] = useState(false); 22 | const [status, setStatus] = useState(0); 23 | const [toolbarStyle, setToolbarStyle] = useState({}); 24 | const [storedSelectionStart, setStoredSelectionStart] = useState(0); 25 | const [storedSelectionEnd, setStoredSelectionEnd] = useState(0); 26 | 27 | /** Views: 28 | * 0: Editor 29 | * 1: End-result 30 | */ 31 | const [view, setView] = useState(0); 32 | const textareaRef = useRef(); 33 | 34 | /** Will load the details of the context and check if user has access to it */ 35 | useEffect(() => { 36 | if(category && category != "") { 37 | loadContextDetails(); 38 | } 39 | 40 | async function loadContextDetails() { 41 | setAccessRulesLoading(true); 42 | setHasAccess(false) 43 | let { data, error } = await orbis.getContext(category); 44 | if(data && data.content) { 45 | /** Save context access rules in state */ 46 | setCategoryAccessRules(data.content.accessRules ? data.content.accessRules : []); 47 | 48 | /** Now check if user has access */ 49 | if(!data.content.accessRules || data.content.accessRules.length == 0) { 50 | setHasAccess(true) 51 | } else { 52 | checkContextAccess(user, credentials, data.content?.accessRules, () => setHasAccess(true)); 53 | } 54 | 55 | } 56 | setAccessRulesLoading(false); 57 | } 58 | }, [category, credentials]); 59 | 60 | /** Triggered on component launch */ 61 | useEffect(() => { 62 | window.addEventListener('scroll', handleScroll); 63 | return () => window.removeEventListener('scroll', handleScroll); 64 | }, []); 65 | 66 | /** Will store the current selection and save in state to make sure we don't lose it when the textarea loses focus because of a click in the format toolbar */ 67 | const storeSelection = () => { 68 | const { selectionStart, selectionEnd } = textareaRef.current; 69 | if(selectionStart) { 70 | setStoredSelectionStart(selectionStart); 71 | } 72 | if(selectionEnd) { 73 | setStoredSelectionEnd(selectionEnd); 74 | } 75 | }; 76 | 77 | /** Will be triggered on scroll to update the toolbar style */ 78 | const handleScroll = () => { 79 | if (textareaRef.current) { 80 | const rect = textareaRef.current.getBoundingClientRect(); 81 | if (rect.top < 0) { 82 | setToolbarStyle({ position: 'fixed', top: 0, marginLeft: 8 }); 83 | } else { 84 | setToolbarStyle({}); 85 | } 86 | } 87 | }; 88 | 89 | 90 | /** Will update title field */ 91 | const handleTitleInputChange = (e) => { 92 | setTitle(e.target.value); 93 | }; 94 | 95 | /** Will update the body field */ 96 | const handleInputChange = (e) => { 97 | setBody(e.target.value); 98 | }; 99 | 100 | const wrapWith = (before, after, newText) => { 101 | const { value, selectionStart, selectionEnd } = textareaRef.current; 102 | let selectedText = newText !== undefined ? newText : value.substring(selectionStart, selectionEnd); 103 | 104 | setBody( 105 | value.substring(0, selectionStart) + 106 | before + 107 | selectedText + 108 | after + 109 | value.substring(selectionEnd) 110 | ); 111 | 112 | // Store the current scroll position 113 | const currentScrollX = window.scrollX; 114 | const currentScrollY = window.scrollY; 115 | 116 | setTimeout(() => { 117 | textareaRef.current.focus(); 118 | textareaRef.current.setSelectionRange( 119 | selectionStart + before.length, 120 | selectionEnd + before.length 121 | ); 122 | 123 | // Restore the scroll position (this is useful to avoid the page to scroll back-up on state/selection update) 124 | window.scrollTo(currentScrollX, currentScrollY); 125 | }, 0); 126 | }; 127 | 128 | // Helper function to toggle formatting 129 | const toggleFormat = (delimiterStart, delimiterEnd, patternStart, patternEnd) => { 130 | const { value } = textareaRef.current; 131 | const beforeSelection = value.substring(0, storedSelectionStart); 132 | const afterSelection = value.substring(storedSelectionEnd); 133 | const selectedText = value.substring(storedSelectionStart, storedSelectionEnd); 134 | 135 | const isFormatted = 136 | (patternStart.test(beforeSelection) && patternEnd.test(afterSelection)) || 137 | (patternStart.test(beforeSelection.slice(-3)) && patternEnd.test(afterSelection.slice(-3))); 138 | 139 | if (isFormatted) { 140 | const newText = 141 | beforeSelection.replace(patternStart, '') + 142 | selectedText + 143 | afterSelection.replace(patternEnd, ''); 144 | setBody(newText); 145 | } else { 146 | wrapWith(delimiterStart, delimiterEnd); 147 | } 148 | }; 149 | 150 | // Toolbar actions 151 | const addBold = () => { 152 | const delimiter = '**'; 153 | const patternStart = /(\*\*|__)$/; 154 | const patternEnd = /^(\*\*|__)/; 155 | toggleFormat(delimiter, delimiter, patternStart, patternEnd); 156 | }; 157 | 158 | const addItalic = () => { 159 | const delimiter = '_'; 160 | const patternStart = /(_)$/; 161 | const patternEnd = /^(_)/; 162 | toggleFormat(delimiter, delimiter, patternStart, patternEnd); 163 | }; 164 | 165 | const addCodeBlock = () => { 166 | const { value } = textareaRef.current; 167 | const selectedText = value.substring(storedSelectionStart, storedSelectionEnd); 168 | 169 | if (selectedText.includes('\n')) { 170 | // Multi-line code block 171 | const delimiterStart = '```\n'; 172 | const delimiterEnd = '\n```'; 173 | const patternStart = /(```)$/; 174 | const patternEnd = /^(```)/; 175 | toggleFormat(delimiterStart, delimiterEnd, patternStart, patternEnd); 176 | } else { 177 | // Single-line code block 178 | const delimiter = '`'; 179 | const patternStart = /(`)$/; 180 | const patternEnd = /^(`)/; 181 | toggleFormat(delimiter, delimiter, patternStart, patternEnd); 182 | } 183 | }; 184 | 185 | const addHeading1 = () => wrapWith('# ', ''); 186 | const addHeading2 = () => wrapWith('## ', ''); 187 | const addHeading3 = () => wrapWith('### ', ''); 188 | const addLink = () => { 189 | const url = prompt('Enter the URL:'); 190 | if (url) { 191 | wrapWith('[', `](${url})`); 192 | } 193 | }; 194 | /** To add a photo to the blog post */ 195 | const addImage = async (event) => { 196 | const file = event.target.files[0]; 197 | 198 | if (file && file.type.match(/^image\//)) { 199 | let res = await orbis.uploadMedia(file); 200 | if(res.status == 200) { 201 | const imgTag = `![Image ALT tag](${getIpfsLink(res.result)})`; 202 | const { value } = textareaRef.current; 203 | setBody( 204 | value.substring(0, storedSelectionStart) + 205 | imgTag + 206 | value.substring(storedSelectionEnd) 207 | ); 208 | } else { 209 | alert("Error uploading image."); 210 | } 211 | } else { 212 | console.log("There isn't any file to upload."); 213 | } 214 | }; 215 | 216 | /** Will edit the post to publish the new version */ 217 | async function updateArticle() { 218 | setStatus(1); 219 | let res; 220 | if(post) { 221 | let _content = {...post.content}; 222 | let _data = {...post.content.data}; 223 | _content.title = title; 224 | _content.body = body; 225 | _content.data = _data; 226 | _content.media = media; 227 | _content.context = category ? category : global.orbis_context; 228 | res = await orbis.editPost(post.stream_id, _content); 229 | console.log("Post updated?", res); 230 | } else { 231 | res = await orbis.createPost({ 232 | title: title, 233 | body: body, 234 | context: category ? category : global.orbis_context, 235 | media: media 236 | }); 237 | console.log("Post created", res); 238 | } 239 | 240 | if(res.status == 200) { 241 | setStatus(2); 242 | await sleep(1500); 243 | router.push("/post/" + res.doc); 244 | } else { 245 | setStatus(3); 246 | await sleep(2500); 247 | setStatus(0); 248 | } 249 | } 250 | 251 | /** Used to upload the main image to IPFS and save it in state */ 252 | const uploadMainImage = async (event) => { 253 | const file = event.target.files[0]; 254 | 255 | if (file && file.type.match(/^image\//)) { 256 | let res = await orbis.uploadMedia(file); 257 | if(res.status == 200) { 258 | setMedia([res.result]) 259 | } else { 260 | alert("Error uploading image."); 261 | } 262 | } 263 | }; 264 | 265 | return ( 266 |
    267 | 268 | {/** Loop categories */} 269 | 270 | 271 | {/** Update view */} 272 | {(category && category != "") && 273 |
    274 |
    275 | 276 | {/** Show preview button only if user started typing a title */} 277 | {title && title != "" && 278 | 279 | } 280 | 281 |
    282 | {post && 283 | View live 284 | } 285 |
    286 | } 287 | 288 | {/** Post Editor or Loading state */} 289 | {view == 0 && 290 |
    291 | {accessRulesLoading ? 292 |
    293 | 294 |
    295 | : 296 |
    297 | {/** Render text inputs only if the category has been selected */} 298 | {(category && category != "") && 299 | <> 300 | {/** If user has access we disply the form */} 301 | {hasAccess ? 302 | <> 303 | {/** Title */} 304 | 310 | 311 | {/** Formatting toolbar container */} 312 |
    313 | 314 | 315 | 316 | 317 | } onClick={addCodeBlock} /> 318 | } onClick={addLink} /> 319 | } onClick={addImage} /> 320 |
    321 | 322 | {/** Actual content of the blog post */} 323 | 331 | 332 | {/** Default status */} 333 | {status == 0 && 334 | <> 335 | {((post && (!user || user.did != post.creator))) ? 336 |
    337 |
    Only
    can update this article.
    338 |
    339 | : 340 | 341 | } 342 | 343 | } 344 | 345 | {/** Loading status */} 346 | {status == 1 && 347 | 348 | } 349 | 350 | {/** success status */} 351 | {status == 2 && 352 | 353 | } 354 | 355 | : 356 |
    357 |

    You can't share a post in this category as it's restricted to certain rules that you do not meet.

    358 | 359 |
    360 | } 361 | 362 | } 363 |
    364 | } 365 | 366 |
    367 | } 368 | 369 | {/** Show live article view */} 370 | {view == 1 && 371 | 381 | } 382 | 383 | {/** Display more details about the access rules required for this context */} 384 | {accessRulesModalVis && 385 | setAccessRulesModalVis(false)} /> 386 | } 387 |
    388 | ); 389 | }; 390 | 391 | /** Will loop through all categories and display them */ 392 | const Categories = ({category, setCategory}) => { 393 | const { orbis, user } = useOrbis(); 394 | const [loading, setLoading] = useState(false); 395 | const [categories, setCategories] = useState([]); 396 | 397 | /** Load all of the categories (sub-contexts) available in this forum */ 398 | useEffect(() => { 399 | loadContexts(); 400 | async function loadContexts() { 401 | setLoading(true); 402 | let { data, error } = await orbis.api.from("orbis_contexts").select().eq('context', global.orbis_context).order('created_at', { ascending: false }); 403 | 404 | setCategories(data); 405 | setLoading(false); 406 | } 407 | }, []); 408 | 409 | return( 410 |
    411 | Which category do you want to share your post into? 412 |
    413 | {categories.map((cat) => { 414 | return ( 415 |
    setCategory(cat.stream_id)}>{cat.content.displayName}
    416 | ); 417 | })} 418 |
    419 |
    420 | ); 421 | } 422 | 423 | /** Simple component to handle the buttons in the toolbar */ 424 | const ToolbarButton = ({label, onClick, isImage}) => { 425 | if(isImage == true) { 426 | return( 427 | <> 428 | 434 | 437 | 438 | ) 439 | } else { 440 | return( 441 | 446 | ) 447 | }; 448 | } 449 | 450 | const ImageIcon = () => { 451 | return( 452 | 453 | 454 | 455 | 456 | 457 | ) 458 | } 459 | 460 | export default Editor; 461 | -------------------------------------------------------------------------------- /components/ArticleContent.jsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import ReactTimeAgo from 'react-time-ago' 3 | import { Orbis, Discussion, User, useOrbis } from "@orbisclub/components"; 4 | import { shortAddress, getIpfsLink } from "../utils"; 5 | import { ExternalLinkIcon, EditIcon } from "./Icons"; 6 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; 7 | 8 | /** For Markdown support */ 9 | import { marked } from 'marked'; 10 | import parse from 'html-react-parser'; 11 | 12 | export default function ArticleContent({post}) { 13 | const { orbis, user } = useOrbis(); 14 | 15 | /** Will replace classic code
     support with a more advanced integration */
     16 |   const replacePreWithSyntaxHighlighter = (node) => {
     17 |     if (node.type === 'tag' && node.name === 'pre') {
     18 |       const codeNode = node.children.find((child) => child.name === 'code');
     19 |       const language = codeNode.attribs.class?.split('-')[1] || ''; // Assumes a format like "language-js"
     20 | 
     21 |       return (
     22 |         
     23 |           {codeNode.children[0].data}
     24 |         
     25 |       );
     26 |     }
     27 |   };
     28 | 
     29 |   const markdownContent = post.content.body.replace(/\n/g, '  \n');
     30 |   const htmlContent = marked(markdownContent);
     31 |   const reactComponent = parse(htmlContent, { replace: replacePreWithSyntaxHighlighter });
     32 | 
     33 | 
     34 |   return(
     35 |     <>
     36 |       
    37 | {/* Post header */} 38 |
    39 |

    {post.content.title}

    40 | 41 | {/** Article Metadata */} 42 |
    43 |
    44 | {/* Post date & creator details */} 45 | · 46 | 47 | · 48 | 49 | {/** Proof link to Cerscan */} 50 | {post.stream_id && 51 | {shortAddress(post.stream_id, 12)} 52 | } 53 | 54 | {/** Edit button if user is creator of the post */} 55 | {(user && user.did == post.creator) && 56 | <> 57 | · 58 | Edit 59 | 60 | } 61 |
    62 |
    63 |
    64 | 65 | {/** Display main image if any */} 66 | {(post.content.media && post.content.media.length > 0) && 67 | {post.content.title} 68 | } 69 | 70 | {/* Post content */} 71 |
    72 | {reactComponent} 73 |
    74 | 75 | {/** Show commenting feed only if not new post */} 76 | {post.stream_id && 77 |
    78 | 79 |
    80 | } 81 | 82 |
    83 | 84 | ) 85 | } 86 | 87 | export const theme = { 88 | "code[class*=\"language-\"]": { 89 | "background": "transparent", 90 | "color": "#FFFFFF", 91 | "fontSize": "#12px", 92 | "textShadow": "0 1px rgba(0, 0, 0, 0.3)", 93 | "direction": "ltr", 94 | "textAlign": "left", 95 | "whiteSpace": "pre-wrap", 96 | "wordSpacing": "normal", 97 | "wordBreak": "normal", 98 | "lineHeight": "1.5", 99 | "MozTabSize": "2", 100 | "OTabSize": "2", 101 | "tabSize": "2", 102 | "WebkitHyphens": "none", 103 | "MozHyphens": "none", 104 | "msHyphens": "none", 105 | "hyphens": "none" 106 | }, 107 | "pre[class*=\"language-\"]": { 108 | "background": "#142438", 109 | "color": "#FFFFFF", 110 | "direction": "ltr", 111 | "textAlign": "left", 112 | "whiteSpace": "pre", 113 | "wordSpacing": "normal", 114 | "wordBreak": "normal", 115 | "lineHeight": "1.5", 116 | "fontSize": "#12px", 117 | "MozTabSize": "2", 118 | "OTabSize": "2", 119 | "tabSize": "2", 120 | "WebkitHyphens": "none", 121 | "MozHyphens": "none", 122 | "msHyphens": "none", 123 | "hyphens": "none", 124 | "padding": "15px", 125 | "overflow": "auto", 126 | "borderRadius": "0.45em" 127 | }, 128 | "code[class*=\"language-\"]::-moz-selection": { 129 | "background": "hsl(220, 13%, 28%)", 130 | "color": "inherit", 131 | "textShadow": "none" 132 | }, 133 | "code[class*=\"language-\"] *::-moz-selection": { 134 | "background": "hsl(220, 13%, 28%)", 135 | "color": "inherit", 136 | "textShadow": "none" 137 | }, 138 | "pre[class*=\"language-\"] *::-moz-selection": { 139 | "background": "hsl(220, 13%, 28%)", 140 | "color": "inherit", 141 | "textShadow": "none" 142 | }, 143 | "code[class*=\"language-\"]::selection": { 144 | "background": "hsl(220, 13%, 28%)", 145 | "color": "inherit", 146 | "textShadow": "none" 147 | }, 148 | "code[class*=\"language-\"] *::selection": { 149 | "background": "hsl(220, 13%, 28%)", 150 | "color": "inherit", 151 | "textShadow": "none" 152 | }, 153 | "pre[class*=\"language-\"] *::selection": { 154 | "background": "hsl(220, 13%, 28%)", 155 | "color": "inherit", 156 | "textShadow": "none" 157 | }, 158 | ":not(pre) > code[class*=\"language-\"]": { 159 | "padding": "0.2em 0.3em", 160 | "borderRadius": "0.3em", 161 | "whiteSpace": "normal" 162 | }, 163 | "comment": { 164 | "color": "#868B95", 165 | "fontStyle": "italic" 166 | }, 167 | "prolog": { 168 | "color": "hsl(220, 10%, 40%)" 169 | }, 170 | "cdata": { 171 | "color": "hsl(220, 10%, 40%)" 172 | }, 173 | "doctype": { 174 | "color": "hsl(220, 14%, 71%)" 175 | }, 176 | "punctuation": { 177 | "color": "hsl(220, 14%, 71%)" 178 | }, 179 | "entity": { 180 | "color": "hsl(220, 14%, 71%)", 181 | "cursor": "help" 182 | }, 183 | "attr-name": { 184 | "color": "#ffd171" 185 | }, 186 | "class-name": { 187 | "color": "#ffd171" 188 | }, 189 | "boolean": { 190 | "color": "#ffd171" 191 | }, 192 | "constant": { 193 | "color": "#ffd171" 194 | }, 195 | "number": { 196 | "color": "#ff9944" 197 | }, 198 | "atrule": { 199 | "color": "#ffd171" 200 | }, 201 | "keyword": { 202 | "color": "#FF99F5" 203 | }, 204 | "property": { 205 | "color": "#FF6DFD" 206 | }, 207 | "tag": { 208 | "color": "#FF6DFD" 209 | }, 210 | "symbol": { 211 | "color": "#FF6DFD" 212 | }, 213 | "deleted": { 214 | "color": "#FF6DFD" 215 | }, 216 | "important": { 217 | "color": "#FF6DFD" 218 | }, 219 | "selector": { 220 | "color": "#c2ff95" 221 | }, 222 | "string": { 223 | "color": "#5CF97C" 224 | }, 225 | "char": { 226 | "color": "#5CF97C" 227 | }, 228 | "builtin": { 229 | "color": "#c2ff95" 230 | }, 231 | "inserted": { 232 | "color": "#c2ff95" 233 | }, 234 | "regex": { 235 | "color": "#c2ff95" 236 | }, 237 | "attr-value": { 238 | "color": "#c2ff95" 239 | }, 240 | "attr-value > .token.punctuation": { 241 | "color": "#c2ff95" 242 | }, 243 | "variable": { 244 | "color": "hsl(207, 82%, 66%)" 245 | }, 246 | "operator": { 247 | "color": "#58B8FF" 248 | }, 249 | "function": { 250 | "color": "#58B8FF" 251 | }, 252 | "url": { 253 | "color": "hsl(187, 47%, 55%)" 254 | }, 255 | "attr-value > .token.punctuation.attr-equals": { 256 | "color": "hsl(220, 14%, 71%)" 257 | }, 258 | "special-attr > .token.attr-value > .token.value.css": { 259 | "color": "hsl(220, 14%, 71%)" 260 | }, 261 | ".language-css .token.selector": { 262 | "color": "#FF6DFD" 263 | }, 264 | ".language-css .token.property": { 265 | "color": "hsl(220, 14%, 71%)" 266 | }, 267 | ".language-css .token.function": { 268 | "color": "hsl(187, 47%, 55%)" 269 | }, 270 | ".language-css .token.url > .token.function": { 271 | "color": "hsl(187, 47%, 55%)" 272 | }, 273 | ".language-css .token.url > .token.string.url": { 274 | "color": "#c2ff95" 275 | }, 276 | ".language-css .token.important": { 277 | "color": "hsl(286, 60%, 67%)" 278 | }, 279 | ".language-css .token.atrule .token.rule": { 280 | "color": "hsl(286, 60%, 67%)" 281 | }, 282 | ".language-javascript .token.operator": { 283 | "color": "hsl(286, 60%, 67%)" 284 | }, 285 | ".language-javascript .token.template-string > .token.interpolation > .token.interpolation-punctuation.punctuation": { 286 | "color": "hsl(5, 48%, 51%)" 287 | }, 288 | ".language-json .token.operator": { 289 | "color": "hsl(220, 14%, 71%)" 290 | }, 291 | ".language-json .token.null.keyword": { 292 | "color": "#ffd171" 293 | }, 294 | ".language-markdown .token.url": { 295 | "color": "hsl(220, 14%, 71%)" 296 | }, 297 | ".language-markdown .token.url > .token.operator": { 298 | "color": "hsl(220, 14%, 71%)" 299 | }, 300 | ".language-markdown .token.url-reference.url > .token.string": { 301 | "color": "hsl(220, 14%, 71%)" 302 | }, 303 | ".language-markdown .token.url > .token.content": { 304 | "color": "hsl(207, 82%, 66%)" 305 | }, 306 | ".language-markdown .token.url > .token.url": { 307 | "color": "hsl(187, 47%, 55%)" 308 | }, 309 | ".language-markdown .token.url-reference.url": { 310 | "color": "hsl(187, 47%, 55%)" 311 | }, 312 | ".language-markdown .token.blockquote.punctuation": { 313 | "color": "hsl(220, 10%, 40%)", 314 | "fontStyle": "italic" 315 | }, 316 | ".language-markdown .token.hr.punctuation": { 317 | "color": "hsl(220, 10%, 40%)", 318 | "fontStyle": "italic" 319 | }, 320 | ".language-markdown .token.code-snippet": { 321 | "color": "#c2ff95" 322 | }, 323 | ".language-markdown .token.bold .token.content": { 324 | "color": "#ffd171" 325 | }, 326 | ".language-markdown .token.italic .token.content": { 327 | "color": "hsl(286, 60%, 67%)" 328 | }, 329 | ".language-markdown .token.strike .token.content": { 330 | "color": "#FF6DFD" 331 | }, 332 | ".language-markdown .token.strike .token.punctuation": { 333 | "color": "#FF6DFD" 334 | }, 335 | ".language-markdown .token.list.punctuation": { 336 | "color": "#FF6DFD" 337 | }, 338 | ".language-markdown .token.title.important > .token.punctuation": { 339 | "color": "#FF6DFD" 340 | }, 341 | "bold": { 342 | "fontWeight": "bold" 343 | }, 344 | "italic": { 345 | "fontStyle": "italic" 346 | }, 347 | "namespace": { 348 | "Opacity": "0.8" 349 | }, 350 | "token.tab:not(:empty):before": { 351 | "color": "hsla(220, 14%, 71%, 0.15)", 352 | "textShadow": "none" 353 | }, 354 | "token.cr:before": { 355 | "color": "hsla(220, 14%, 71%, 0.15)", 356 | "textShadow": "none" 357 | }, 358 | "token.lf:before": { 359 | "color": "hsla(220, 14%, 71%, 0.15)", 360 | "textShadow": "none" 361 | }, 362 | "token.space:before": { 363 | "color": "hsla(220, 14%, 71%, 0.15)", 364 | "textShadow": "none" 365 | }, 366 | "div.code-toolbar > .toolbar.toolbar > .toolbar-item": { 367 | "marginRight": "0.4em" 368 | }, 369 | "div.code-toolbar > .toolbar.toolbar > .toolbar-item > button": { 370 | "background": "hsl(220, 13%, 26%)", 371 | "color": "hsl(220, 9%, 55%)", 372 | "padding": "0.1em 0.4em", 373 | "borderRadius": "0.3em" 374 | }, 375 | "div.code-toolbar > .toolbar.toolbar > .toolbar-item > a": { 376 | "background": "hsl(220, 13%, 26%)", 377 | "color": "hsl(220, 9%, 55%)", 378 | "padding": "0.1em 0.4em", 379 | "borderRadius": "0.3em" 380 | }, 381 | "div.code-toolbar > .toolbar.toolbar > .toolbar-item > span": { 382 | "background": "hsl(220, 13%, 26%)", 383 | "color": "hsl(220, 9%, 55%)", 384 | "padding": "0.1em 0.4em", 385 | "borderRadius": "0.3em" 386 | }, 387 | "div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:hover": { 388 | "background": "hsl(220, 13%, 28%)", 389 | "color": "hsl(220, 14%, 71%)" 390 | }, 391 | "div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:focus": { 392 | "background": "hsl(220, 13%, 28%)", 393 | "color": "hsl(220, 14%, 71%)" 394 | }, 395 | "div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:hover": { 396 | "background": "hsl(220, 13%, 28%)", 397 | "color": "hsl(220, 14%, 71%)" 398 | }, 399 | "div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:focus": { 400 | "background": "hsl(220, 13%, 28%)", 401 | "color": "hsl(220, 14%, 71%)" 402 | }, 403 | "div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:hover": { 404 | "background": "hsl(220, 13%, 28%)", 405 | "color": "hsl(220, 14%, 71%)" 406 | }, 407 | "div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:focus": { 408 | "background": "hsl(220, 13%, 28%)", 409 | "color": "hsl(220, 14%, 71%)" 410 | }, 411 | ".line-highlight.line-highlight": { 412 | "background": "hsla(220, 100%, 80%, 0.04)" 413 | }, 414 | ".line-highlight.line-highlight:before": { 415 | "background": "hsl(220, 13%, 26%)", 416 | "color": "hsl(220, 14%, 71%)", 417 | "padding": "0.1em 0.6em", 418 | "borderRadius": "0.3em", 419 | "boxShadow": "0 2px 0 0 rgba(0, 0, 0, 0.2)" 420 | }, 421 | ".line-highlight.line-highlight[data-end]:after": { 422 | "background": "hsl(220, 13%, 26%)", 423 | "color": "hsl(220, 14%, 71%)", 424 | "padding": "0.1em 0.6em", 425 | "borderRadius": "0.3em", 426 | "boxShadow": "0 2px 0 0 rgba(0, 0, 0, 0.2)" 427 | }, 428 | "pre[id].linkable-line-numbers.linkable-line-numbers span.line-numbers-rows > span:hover:before": { 429 | "backgroundColor": "hsla(220, 100%, 80%, 0.04)" 430 | }, 431 | ".line-numbers.line-numbers .line-numbers-rows": { 432 | "borderRightColor": "hsla(220, 14%, 71%, 0.15)" 433 | }, 434 | ".command-line .command-line-prompt": { 435 | "borderRightColor": "hsla(220, 14%, 71%, 0.15)" 436 | }, 437 | ".line-numbers .line-numbers-rows > span:before": { 438 | "color": "hsl(220, 14%, 45%)" 439 | }, 440 | ".command-line .command-line-prompt > span:before": { 441 | "color": "hsl(220, 14%, 45%)" 442 | }, 443 | ".rainbow-braces .token.token.punctuation.brace-level-1": { 444 | "color": "#FF6DFD" 445 | }, 446 | ".rainbow-braces .token.token.punctuation.brace-level-5": { 447 | "color": "#FF6DFD" 448 | }, 449 | ".rainbow-braces .token.token.punctuation.brace-level-9": { 450 | "color": "#FF6DFD" 451 | }, 452 | ".rainbow-braces .token.token.punctuation.brace-level-2": { 453 | "color": "#c2ff95" 454 | }, 455 | ".rainbow-braces .token.token.punctuation.brace-level-6": { 456 | "color": "#c2ff95" 457 | }, 458 | ".rainbow-braces .token.token.punctuation.brace-level-10": { 459 | "color": "#c2ff95" 460 | }, 461 | ".rainbow-braces .token.token.punctuation.brace-level-3": { 462 | "color": "hsl(207, 82%, 66%)" 463 | }, 464 | ".rainbow-braces .token.token.punctuation.brace-level-7": { 465 | "color": "hsl(207, 82%, 66%)" 466 | }, 467 | ".rainbow-braces .token.token.punctuation.brace-level-11": { 468 | "color": "hsl(207, 82%, 66%)" 469 | }, 470 | ".rainbow-braces .token.token.punctuation.brace-level-4": { 471 | "color": "hsl(286, 60%, 67%)" 472 | }, 473 | ".rainbow-braces .token.token.punctuation.brace-level-8": { 474 | "color": "hsl(286, 60%, 67%)" 475 | }, 476 | ".rainbow-braces .token.token.punctuation.brace-level-12": { 477 | "color": "hsl(286, 60%, 67%)" 478 | }, 479 | "pre.diff-highlight > code .token.token.deleted:not(.prefix)": { 480 | "backgroundColor": "hsla(353, 100%, 66%, 0.15)" 481 | }, 482 | "pre > code.diff-highlight .token.token.deleted:not(.prefix)": { 483 | "backgroundColor": "hsla(353, 100%, 66%, 0.15)" 484 | }, 485 | "pre.diff-highlight > code .token.token.deleted:not(.prefix)::-moz-selection": { 486 | "backgroundColor": "hsla(353, 95%, 66%, 0.25)" 487 | }, 488 | "pre.diff-highlight > code .token.token.deleted:not(.prefix) *::-moz-selection": { 489 | "backgroundColor": "hsla(353, 95%, 66%, 0.25)" 490 | }, 491 | "pre > code.diff-highlight .token.token.deleted:not(.prefix)::-moz-selection": { 492 | "backgroundColor": "hsla(353, 95%, 66%, 0.25)" 493 | }, 494 | "pre > code.diff-highlight .token.token.deleted:not(.prefix) *::-moz-selection": { 495 | "backgroundColor": "hsla(353, 95%, 66%, 0.25)" 496 | }, 497 | "pre.diff-highlight > code .token.token.deleted:not(.prefix)::selection": { 498 | "backgroundColor": "hsla(353, 95%, 66%, 0.25)" 499 | }, 500 | "pre.diff-highlight > code .token.token.deleted:not(.prefix) *::selection": { 501 | "backgroundColor": "hsla(353, 95%, 66%, 0.25)" 502 | }, 503 | "pre > code.diff-highlight .token.token.deleted:not(.prefix)::selection": { 504 | "backgroundColor": "hsla(353, 95%, 66%, 0.25)" 505 | }, 506 | "pre > code.diff-highlight .token.token.deleted:not(.prefix) *::selection": { 507 | "backgroundColor": "hsla(353, 95%, 66%, 0.25)" 508 | }, 509 | "pre.diff-highlight > code .token.token.inserted:not(.prefix)": { 510 | "backgroundColor": "hsla(137, 100%, 55%, 0.15)" 511 | }, 512 | "pre > code.diff-highlight .token.token.inserted:not(.prefix)": { 513 | "backgroundColor": "hsla(137, 100%, 55%, 0.15)" 514 | }, 515 | "pre.diff-highlight > code .token.token.inserted:not(.prefix)::-moz-selection": { 516 | "backgroundColor": "hsla(135, 73%, 55%, 0.25)" 517 | }, 518 | "pre.diff-highlight > code .token.token.inserted:not(.prefix) *::-moz-selection": { 519 | "backgroundColor": "hsla(135, 73%, 55%, 0.25)" 520 | }, 521 | "pre > code.diff-highlight .token.token.inserted:not(.prefix)::-moz-selection": { 522 | "backgroundColor": "hsla(135, 73%, 55%, 0.25)" 523 | }, 524 | "pre > code.diff-highlight .token.token.inserted:not(.prefix) *::-moz-selection": { 525 | "backgroundColor": "hsla(135, 73%, 55%, 0.25)" 526 | }, 527 | "pre.diff-highlight > code .token.token.inserted:not(.prefix)::selection": { 528 | "backgroundColor": "hsla(135, 73%, 55%, 0.25)" 529 | }, 530 | "pre.diff-highlight > code .token.token.inserted:not(.prefix) *::selection": { 531 | "backgroundColor": "hsla(135, 73%, 55%, 0.25)" 532 | }, 533 | "pre > code.diff-highlight .token.token.inserted:not(.prefix)::selection": { 534 | "backgroundColor": "hsla(135, 73%, 55%, 0.25)" 535 | }, 536 | "pre > code.diff-highlight .token.token.inserted:not(.prefix) *::selection": { 537 | "backgroundColor": "hsla(135, 73%, 55%, 0.25)" 538 | }, 539 | ".prism-previewer.prism-previewer:before": { 540 | "borderColor": "hsl(224, 13%, 17%)" 541 | }, 542 | ".prism-previewer-gradient.prism-previewer-gradient div": { 543 | "borderColor": "hsl(224, 13%, 17%)", 544 | "borderRadius": "0.3em" 545 | }, 546 | ".prism-previewer-color.prism-previewer-color:before": { 547 | "borderRadius": "0.3em" 548 | }, 549 | ".prism-previewer-easing.prism-previewer-easing:before": { 550 | "borderRadius": "0.3em" 551 | }, 552 | ".prism-previewer.prism-previewer:after": { 553 | "borderTopColor": "hsl(224, 13%, 17%)" 554 | }, 555 | ".prism-previewer-flipped.prism-previewer-flipped.after": { 556 | "borderBottomColor": "hsl(224, 13%, 17%)" 557 | }, 558 | ".prism-previewer-angle.prism-previewer-angle:before": { 559 | "background": "hsl(219, 13%, 22%)" 560 | }, 561 | ".prism-previewer-time.prism-previewer-time:before": { 562 | "background": "hsl(219, 13%, 22%)" 563 | }, 564 | ".prism-previewer-easing.prism-previewer-easing": { 565 | "background": "hsl(219, 13%, 22%)" 566 | }, 567 | ".prism-previewer-angle.prism-previewer-angle circle": { 568 | "stroke": "hsl(220, 14%, 71%)", 569 | "strokeOpacity": "1" 570 | }, 571 | ".prism-previewer-time.prism-previewer-time circle": { 572 | "stroke": "hsl(220, 14%, 71%)", 573 | "strokeOpacity": "1" 574 | }, 575 | ".prism-previewer-easing.prism-previewer-easing circle": { 576 | "stroke": "hsl(220, 14%, 71%)", 577 | "fill": "transparent" 578 | }, 579 | ".prism-previewer-easing.prism-previewer-easing path": { 580 | "stroke": "hsl(220, 14%, 71%)" 581 | }, 582 | ".prism-previewer-easing.prism-previewer-easing line": { 583 | "stroke": "hsl(220, 14%, 71%)" 584 | } 585 | }; 586 | -------------------------------------------------------------------------------- /components/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | import Link from 'next/link'; 3 | import { Logo, PanelRight, SearchIcon, MenuVerticalIcon, LoadingCircle } from "./Icons"; 4 | import useOutsideClick from "../hooks/useOutsideClick"; 5 | import { useOrbis, User, UserPopup, Chat, Post } from "@orbisclub/components"; 6 | import { getTimestamp } from "../utils"; 7 | 8 | function Header() { 9 | const { orbis, user, connecting, setConnectModalVis } = useOrbis(); 10 | const [showCommunityChat, setShowCommunityChat] = useState(false); 11 | const [showUserMenu, setShowUserMenu] = useState(false); 12 | const [hasUnreadMessages, setHasUnreadMessages] = useState(false); 13 | 14 | useEffect(() => { 15 | getLastTimeRead(); 16 | 17 | async function getLastTimeRead() { 18 | /** Retrieve last post timestamp for this context */ 19 | let { data, error } = await orbis.getContext(global.orbis_chat_context); 20 | 21 | /** Retrieve last read time for user */ 22 | let last_read = localStorage.getItem(global.orbis_chat_context + "-last-read"); 23 | if(last_read) { 24 | last_read = parseInt(last_read); 25 | } else { 26 | last_read = 0; 27 | } 28 | 29 | /** Show unread messages indicator if applicable */ 30 | if(data && data.last_post_timestamp && (data.last_post_timestamp > last_read)) { 31 | setHasUnreadMessages(true); 32 | } 33 | } 34 | }, []); 35 | 36 | /** Open community chat and reset new message indicator */ 37 | function openCommunityChat() { 38 | setShowCommunityChat(true); 39 | setHasUnreadMessages(false); 40 | } 41 | 42 | return ( 43 | <> 44 |
    45 |
    46 |
    47 | {/* Site branding */} 48 |
    49 | {/* Logo container */} 50 | 51 | 52 | 53 |
    54 | 55 | {/* Desktop navigation */} 56 | 110 |
    111 |
    112 |
    113 | 114 | {/** Show community chat if enabled */} 115 | {showCommunityChat && 116 | setShowCommunityChat(false)} /> 117 | } 118 | 119 | ); 120 | } 121 | 122 | /** Badhe showing the new notifications count if any */ 123 | const BadgeNotifications = () => { 124 | const { orbis } = useOrbis(); 125 | const [countNewNotifs, setCountNewNotifs] = useState(); 126 | const [showNotifPane, setShowNotifPane] = useState(false); 127 | 128 | /** Will check if user has new notifications for this context */ 129 | useEffect(() => { 130 | const interval = setInterval(loadNotifications, 5000); // run loadNotifications every 5 seconds 131 | return () => clearInterval(interval); // cleanup function to stop the interval when the component unmounts 132 | }, []); 133 | 134 | async function loadNotifications() { 135 | try { 136 | const { data } = await orbis.getNotificationsCount({ 137 | type: "social", 138 | context: global.orbis_context, 139 | include_child_contexts: true 140 | }); 141 | setCountNewNotifs(data.count_new_notifications); 142 | } catch (error) { 143 | console.log("Error loading notifications:", error); 144 | } 145 | } 146 | 147 | return( 148 |
    setShowNotifPane(true)}> 149 |
    150 | 151 | 152 | 153 | {(countNewNotifs > 0) && 154 |
    {countNewNotifs}
    155 | } 156 |
    157 | 158 | {/** Show notifications pane */} 159 | {showNotifPane && 160 | setShowNotifPane(false)} /> 161 | } 162 |
    163 | ); 164 | } 165 | 166 | /** Pane with the user's notifications for this context */ 167 | const NotificationsPane = ({setCountNewNotifs, hide}) => { 168 | const { orbis } = useOrbis(); 169 | const wrapperRef = useRef(null); 170 | const [notifications, setNotifications] = useState([]); 171 | const [notificationsLoading, setNotificationsLoading] = useState(false); 172 | 173 | useEffect(() => { 174 | loadNotifications() 175 | async function loadNotifications() { 176 | setNotificationsLoading(true); 177 | let { data, error } = await orbis.getNotifications({ 178 | type: "social", 179 | context: global.orbis_context, 180 | include_child_contexts: true 181 | }); 182 | 183 | if(error) { 184 | console.log("error getNotifications:", error); 185 | } 186 | 187 | if(data) { 188 | setNotifications(data); 189 | } else { 190 | setNotifications([]); 191 | } 192 | 193 | setNotificationsLoading(false); 194 | setCountNewNotifs(0); 195 | 196 | /** Save new notification's last read timestamp */ 197 | let _content = { 198 | type: "social", 199 | context: global.orbis_context, 200 | timestamp: parseInt(getTimestamp()) 201 | }; 202 | let res = await orbis.setNotificationsReadTime(_content); 203 | console.log("res:", res) 204 | } 205 | }, []) 206 | 207 | /** Is triggered when clicked outside the component */ 208 | useOutsideClick(wrapperRef, () => hide()); 209 | return( 210 |
    211 |
    212 | {notificationsLoading ? 213 |
    214 | 215 |
    216 | : 217 | <> 218 | {notifications.length > 0 ? 219 | <> 220 | {notifications.map((notification, key) => { 221 | return ( 222 | 223 | ); 224 | })} 225 | 226 | : 227 |

    You don't have any notifications here.

    228 | } 229 | 230 | } 231 |
    232 |
    233 | ) 234 | } 235 | 236 | /** Component for the notification item */ 237 | const NotificationItem = ({notification}) => { 238 | 239 | /** Returns a clean name for the notification type */ 240 | const NotificationFamily = () => { 241 | switch (notification.family) { 242 | case "reply_to": 243 | return <>replied:; 244 | case "follow": 245 | return <>is following you.; 246 | case "reaction": 247 | return <>:; 248 | default: 249 | return notification.family; 250 | } 251 | } 252 | 253 | const Reaction = () => { 254 | switch (notification.content?.type) { 255 | case "like": 256 | return( 257 | 258 | 259 | 260 | ) 261 | case "haha": 262 | return HAHA! 263 | default: 264 | 265 | } 266 | } 267 | 268 | return( 269 |
    270 |
    271 | 272 | 273 |
    274 | {(notification.family == "reply_to" || notification.family == "reaction") && 275 |
    276 | {(notification.post_details && notification.post_details.content && notification.post_details.content.body) ? 277 | 278 | : 279 |

    Post deleted...

    280 | } 281 |
    282 | } 283 | 284 |
    285 | ) 286 | } 287 | 288 | /** User menu with update profile and logout buttons */ 289 | const UserMenuVertical = ({hide}) => { 290 | const { orbis, user, setUser } = useOrbis(); 291 | const [showUserPopup, setShowUserPopup] = useState(false); 292 | const wrapperRef = useRef(null); 293 | 294 | /** Is triggered when clicked outside the component */ 295 | useOutsideClick(wrapperRef, () => hide()); 296 | 297 | async function logout() { 298 | let res = await orbis.logout(); 299 | setUser(null); 300 | hide(); 301 | } 302 | 303 | return( 304 | <> 305 |
    306 |
    307 |
    setShowUserPopup(true)}>Update profile
    308 |
    logout()}>Logout
    309 | 310 | {showUserPopup && 311 | setShowUserPopup(false)}> 312 |
    313 | 314 |
    315 |
    316 | } 317 |
    318 |
    319 | 320 | ) 321 | } 322 | 323 | /** Search form */ 324 | const SearchBar = () => { 325 | const { orbis } = useOrbis(); 326 | const [search, setSearch] = useState(""); 327 | const [loading, setLoading] = useState(false); 328 | const [posts, setPosts] = useState([]); 329 | 330 | useEffect(() => { 331 | if(search && search.length > 2) { 332 | loadPosts(); 333 | } else { 334 | setPosts([]); 335 | } 336 | 337 | async function loadPosts() { 338 | setLoading(true); 339 | let { data, error } = await orbis.getPosts({ 340 | context: global.orbis_context, 341 | include_child_contexts: true, 342 | term: search 343 | }); 344 | if(error) { 345 | console.log("error:", error); 346 | } 347 | 348 | setPosts(data); 349 | setLoading(false); 350 | } 351 | }, [search]); 352 | 353 | return( 354 |
    355 |
    356 |
    357 |
    358 | setSearch(e.target.value)} /> 359 |
    360 | 361 |
    362 |
    363 | {/** Show search results */} 364 | {(search && search.length > 2) && 365 |
    366 |
    367 | {loading ? 368 |
    369 | 370 |
    371 | : 372 | <> 373 | {(posts && posts.length > 0) ? 374 | <> 375 | {posts.map((post, key) => { 376 | if(post.content.master) { 377 | return ( 378 | · {post.content.title ? post.content.title : <>{post.content.body.substring(0,25)}...} 379 | ); 380 | } else { 381 | return ( 382 | {post.content.title ? post.content.title : <>{post.content.body.substring(0,25)}...} 383 | ); 384 | } 385 | 386 | })} 387 | 388 | : 389 |
    There isn't any result for this query.
    390 | } 391 | 392 | } 393 |
    394 |
    395 | } 396 |
    397 |
    398 |
    399 | ) 400 | } 401 | 402 | /** Container for the community chat panel */ 403 | const ChatPanel = ({hide}) => { 404 | const wrapperRef = useRef(null); 405 | 406 | /** Is triggered when clicked outside the component */ 407 | useOutsideClick(wrapperRef, () => hide()); 408 | 409 | return( 410 |
    411 |
    412 |
    413 |
    414 |
    415 |
    416 |
    417 |
    418 |
    419 |

    Community Chat

    420 |
    421 | 427 |
    428 |
    429 |
    430 |

    Participate in dynamic conversations with other community members in real-time.

    431 |
    432 |
    433 |
    434 |
    435 | 439 |
    440 |
    441 |
    442 |
    443 |
    444 |
    445 |
    446 |
    447 | ) 448 | } 449 | 450 | /** Background wrapper used to surround modals or side panels */ 451 | const BackgroundWrapper = ({hide, children}) => { 452 | const wrapperRef = useRef(null); 453 | 454 | /** Is triggered when clicked outside the component */ 455 | useOutsideClick(wrapperRef, () => hide()); 456 | return( 457 |
    458 |
    459 |
    hide()}>
    460 | {children} 461 |
    462 |
    463 | ) 464 | } 465 | 466 | export default Header; 467 | --------------------------------------------------------------------------------