├── .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 |
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 |
9 |
10 |
11 |
12 | {/* Hero content */}
13 |
14 | {/* Content */}
15 |
16 | {/* Copy */}
17 |
18 |
{title}
19 |
{description}
20 |
21 |
22 | {/* Image */}
23 |
24 | {image}
25 |
26 |
27 |
28 |
29 |
30 |
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 |
setConnectModalVis(true)}>Connect
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 |
setConnectModalVis(true)}>Connect
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 |
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 |
135 | }
136 |
137 |
138 | );
139 | }
140 |
141 | const NewsletterBlock = () => {
142 | return(
143 |
144 |
145 |
147 |
148 | Receive our forum updates by email:
149 |
150 | {/* Form */}
151 |
152 |
153 | Email
154 |
155 |
163 |
Your email will be encrypted.
164 |
165 |
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 | setPage(page + 1)}>
128 | Next page ->
129 |
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 = `})`;
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 | setView(0)}>Editor
276 | {/** Show preview button only if user started typing a title */}
277 | {title && title != "" &&
278 | setView(1)}>Preview
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 |
updateArticle()}>{post ? "Update" : "Share"}
341 | }
342 | >
343 | }
344 |
345 | {/** Loading status */}
346 | {status == 1 &&
347 |
Loading...
348 | }
349 |
350 | {/** success status */}
351 | {status == 2 &&
352 |
Success
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 |
setAccessRulesModalVis(true)}>View rules
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 | {label}
437 | >
438 | )
439 | } else {
440 | return(
441 |
444 | {label}
445 |
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 |
64 |
65 | {/** Display main image if any */}
66 | {(post.content.media && post.content.media.length > 0) &&
67 |
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 |
57 | {/* Desktop sign in links */}
58 |
59 |
60 |
61 |
62 |
63 |
65 | Learn more
66 |
67 |
68 | {/** Show connect button or user connected */}
69 | {user ?
70 |
71 |
72 | {/** User CTA */}
73 | setShowUserMenu(true)}>
74 |
75 |
76 |
77 |
78 | {/** Notifications icon */}
79 |
80 |
81 | {/** Showing user menu */}
82 | {showUserMenu &&
83 | setShowUserMenu(false)} />
84 | }
85 |
86 | :
87 |
88 | {connecting ?
89 | setConnectModalVis(true)}> Connecting
90 | :
91 | setConnectModalVis(true)}>Connect
92 | }
93 |
94 |
95 | }
96 | {/** Will open the discussion feed on the right */}
97 |
98 | openCommunityChat()}>
99 | Community Chat
100 |
101 | {/** Show unread indicator if any */}
102 | {hasUnreadMessages &&
103 |
104 | }
105 |
106 |
107 |
108 |
109 |
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 |
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 |
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 |
hide()}>
422 | Close panel
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
Participate in dynamic conversations with other community members in real-time.
431 |
432 |
433 |
434 |
435 |
436 | {/** Chat feed */}
437 |
438 |
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 |
--------------------------------------------------------------------------------