├── src ├── types │ ├── types.d.ts │ ├── actions.ts │ └── app.ts ├── react-app-env.d.ts ├── utils │ ├── getSpaceName.ts │ ├── localStorageTest.ts │ ├── timeConverter.ts │ ├── createThread.ts │ ├── socialShare.ts │ ├── parseMessage.ts │ ├── useQueryStringParams.ts │ └── createTheme.ts ├── fonts │ ├── OpenSans-Bold.ttf │ ├── OpenSans-Regular.ttf │ ├── Merriweather-Black.ttf │ ├── Merriweather-Bold.ttf │ ├── OpenSans-SemiBold.ttf │ └── Merriweather-Regular.ttf ├── config │ └── showdown.ts ├── services │ ├── appContext.ts │ ├── appReducer.ts │ ├── userActions.ts │ └── blogActions.ts ├── index.tsx ├── styles │ ├── components │ │ ├── Editor.styles.ts │ │ ├── BookmarkShare.styles.ts │ │ ├── PostPagination.styles.ts │ │ ├── Footer.styles.ts │ │ ├── WalletModal.styles.ts │ │ ├── LikeShare.styles.ts │ │ ├── SharePopup.styles.ts │ │ ├── PostPreview.styles.ts │ │ └── Header.styles.ts │ ├── pages │ │ ├── Home.styles.ts │ │ ├── Drafts.styles.ts │ │ ├── Read.styles.ts │ │ └── Write.styles.ts │ ├── App.styles.ts │ └── index.styles.css ├── images │ ├── arrow-right.svg │ ├── bookmark-add.svg │ ├── bookmarks.svg │ ├── facebook.svg │ ├── medium.svg │ ├── logout-circle.svg │ ├── file-draft.svg │ ├── linkedin.svg │ ├── instagram.svg │ ├── trash-empty.svg │ ├── cloud-upload.svg │ ├── user.svg │ ├── telegram.svg │ ├── pencil-create.svg │ ├── pencil-edit.svg │ ├── thumbs-up.svg │ ├── twitter.svg │ ├── three-box-logo-dark.svg │ ├── three-box-logo-light.svg │ ├── unstoppable-domains-logo-dark.svg │ └── unstoppable-domains-logo-light.svg ├── components │ ├── BookmarkAdd.tsx │ ├── Editor.tsx │ ├── SharePopup.tsx │ ├── WalletModal.tsx │ ├── Footer.tsx │ ├── PostPagination.tsx │ ├── BookmarkShare.tsx │ ├── Header │ │ ├── index.tsx │ │ ├── StandardHeader.tsx │ │ └── MobileHeader.tsx │ ├── LikeShare.tsx │ ├── PostPreview.tsx │ ├── AvatarMenu.tsx │ └── CustomIcon.tsx ├── Init.tsx ├── pages │ ├── Home.tsx │ ├── Drafts.tsx │ ├── Bookmarks.tsx │ ├── Read.tsx │ └── Write.tsx └── App.tsx ├── public ├── logo.png ├── favicon.ico ├── manifest.json ├── config.json └── index.html ├── .gitignore ├── .github └── workflows │ └── always_fail_workflow.yaml ├── tsconfig.json ├── website-builder ├── package.json └── README.md /src/types/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "3box"; 2 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unstoppabledomains/3box-blog-example/HEAD/public/logo.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unstoppabledomains/3box-blog-example/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/utils/getSpaceName.ts: -------------------------------------------------------------------------------- 1 | export default (domain: string) => `basic-blog-${domain.replace(".", "#")}`; 2 | -------------------------------------------------------------------------------- /src/fonts/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unstoppabledomains/3box-blog-example/HEAD/src/fonts/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /src/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unstoppabledomains/3box-blog-example/HEAD/src/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /src/fonts/Merriweather-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unstoppabledomains/3box-blog-example/HEAD/src/fonts/Merriweather-Black.ttf -------------------------------------------------------------------------------- /src/fonts/Merriweather-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unstoppabledomains/3box-blog-example/HEAD/src/fonts/Merriweather-Bold.ttf -------------------------------------------------------------------------------- /src/fonts/OpenSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unstoppabledomains/3box-blog-example/HEAD/src/fonts/OpenSans-SemiBold.ttf -------------------------------------------------------------------------------- /src/fonts/Merriweather-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unstoppabledomains/3box-blog-example/HEAD/src/fonts/Merriweather-Regular.ttf -------------------------------------------------------------------------------- /src/config/showdown.ts: -------------------------------------------------------------------------------- 1 | export const showdownOptions = { 2 | tables: true, 3 | emoji: true, 4 | strikethrough: true, 5 | openLinksInNewWindow: true, 6 | extensions: [], 7 | }; 8 | -------------------------------------------------------------------------------- /src/utils/localStorageTest.ts: -------------------------------------------------------------------------------- 1 | export default () => { 2 | var test = "test"; 3 | try { 4 | localStorage.setItem(test, test); 5 | localStorage.removeItem(test); 6 | return true; 7 | } catch (e) { 8 | return false; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/services/appContext.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { AppContext, initialState } from "types/app"; 3 | import { AppAction } from "types/actions"; 4 | 5 | const appContext = React.createContext({ 6 | state: initialState, 7 | dispatch: (action: AppAction) => { 8 | console.log("No dispatch", action); 9 | }, 10 | }); 11 | 12 | export default appContext; 13 | -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # IDEs and editors 15 | /.idea 16 | /.vscode 17 | 18 | # misc 19 | .DS_Store 20 | .env 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | /test-posts -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "reseller-test-don0.crypto", 3 | "name": "Don's Amazing Blog", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme": { 14 | "colors": { 15 | "primary": "#fff", 16 | "secondary": "#ffeb3b", 17 | "background": "#000", 18 | "icon_color": "#000" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import "fonts/Merriweather-Black.ttf"; 5 | import "fonts/Merriweather-Bold.ttf"; 6 | import "fonts/Merriweather-Regular.ttf"; 7 | import "fonts/OpenSans-Bold.ttf"; 8 | import "fonts/OpenSans-SemiBold.ttf"; 9 | import "fonts/OpenSans-Regular.ttf"; 10 | import "styles/index.styles.css"; 11 | // import Init from "./Init"; 12 | 13 | ReactDOM.render(, document.querySelector("#root")); 14 | // ReactDOM.render(, document.querySelector("#root")); 15 | -------------------------------------------------------------------------------- /src/styles/components/Editor.styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, createStyles, Theme } from "@material-ui/core/styles"; 2 | 3 | export default makeStyles((theme: Theme) => 4 | createStyles({ 5 | textArea: { 6 | outline: "none", 7 | }, 8 | root: { 9 | "&:hover": { 10 | outline: `1px solid ${theme.palette.primary.main}`, 11 | borderColor: "rgba(0,0,0,0)", 12 | }, 13 | "&:focus-within": { 14 | outline: `2px solid ${theme.palette.primary.main}`, 15 | borderColor: "rgba(0,0,0,0)", 16 | }, 17 | }, 18 | }) 19 | ); 20 | -------------------------------------------------------------------------------- /src/styles/components/BookmarkShare.styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, Theme, createStyles } from "@material-ui/core/styles"; 2 | 3 | export default makeStyles((theme: Theme) => 4 | createStyles({ 5 | container: { 6 | display: "flex", 7 | flexDirection: "row", 8 | alignItems: "center", 9 | marginRight: theme.spacing(-1), 10 | [theme.breakpoints.down("xs")]: { 11 | flexDirection: "column", 12 | }, 13 | }, 14 | icon: { 15 | padding: `${theme.spacing(1)}px !important`, 16 | marginLeft: theme.spacing(1), 17 | }, 18 | }) 19 | ); 20 | -------------------------------------------------------------------------------- /src/utils/timeConverter.ts: -------------------------------------------------------------------------------- 1 | export default (timestamp: number) => { 2 | const d = new Date(timestamp * 1000); 3 | const months = [ 4 | "Jan", 5 | "Feb", 6 | "Mar", 7 | "Apr", 8 | "May", 9 | "Jun", 10 | "Jul", 11 | "Aug", 12 | "Sep", 13 | "Oct", 14 | "Nov", 15 | "Dec", 16 | ]; 17 | const year = d.getFullYear(); 18 | const month = months[d.getMonth()]; 19 | const date = d.getDate(); 20 | const hour = d.getHours(); 21 | const min = d.getMinutes(); 22 | const time = date + " " + month + " " + year + " " + hour + ":" + min; 23 | return time; 24 | }; 25 | -------------------------------------------------------------------------------- /.github/workflows/always_fail_workflow.yaml: -------------------------------------------------------------------------------- 1 | name: Always Fail Status Check 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | NODE_VERSION: 16.15.0 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest] 15 | node: [16] 16 | 17 | name: Always Fail until proper checks are in place 18 | 19 | steps: 20 | - name: Checkout repo 21 | uses: actions/checkout@v4 22 | 23 | - name: Informational Message 24 | run: echo You need to setup proper branch protection rules and status checks 25 | 26 | - name: Force Fail 27 | run: exit 99 -------------------------------------------------------------------------------- /src/images/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 / arrows / arrow-right 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "preserve", 17 | "baseUrl": "./src", 18 | "downlevelIteration": true 19 | }, 20 | "include": ["**/*.d.ts", "src/types/*.d.ts", "**/*.ts", "**/*.tsx"] 21 | } 22 | -------------------------------------------------------------------------------- /src/styles/pages/Home.styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, Theme, createStyles } from "@material-ui/core/styles"; 2 | 3 | export default makeStyles((theme: Theme) => 4 | createStyles({ 5 | root: { 6 | display: "flex", 7 | padding: theme.spacing(2), 8 | maxWidth: 800, 9 | marginLeft: "auto", 10 | marginRight: "auto", 11 | flexDirection: "column", 12 | [theme.breakpoints.down("xs")]: { 13 | padding: 0, 14 | }, 15 | }, 16 | center: { 17 | height: "50vh", 18 | width: "100%", 19 | display: "flex", 20 | justifyContent: "center", 21 | alignItems: "center", 22 | }, 23 | }) 24 | ); 25 | -------------------------------------------------------------------------------- /src/components/BookmarkAdd.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface Props { 4 | color: string; 5 | } 6 | 7 | const BookmarkAdd: React.FunctionComponent = ({ color }) => ( 8 | 14 | 15 | 16 | 20 | 21 | 22 | ); 23 | 24 | export default BookmarkAdd; 25 | -------------------------------------------------------------------------------- /src/styles/App.styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, createStyles, Theme } from "@material-ui/core/styles"; 2 | 3 | export default makeStyles((theme: Theme) => 4 | createStyles({ 5 | root: { 6 | minHeight: `calc(100vh - (64px + 163px))`, // 100vh - (header + footer) 7 | maxWidth: 960, 8 | padding: theme.spacing(4, 2), 9 | marginTop: theme.spacing(9), 10 | marginRight: "auto", 11 | marginLeft: "auto", 12 | }, 13 | loadingContainer: { 14 | marginRight: "auto", 15 | marginLeft: "auto", 16 | display: "flex", 17 | alignItems: "center", 18 | justifyContent: "center", 19 | height: "97vh", 20 | overflow: "hidden", 21 | }, 22 | }) 23 | ); 24 | -------------------------------------------------------------------------------- /src/styles/components/PostPagination.styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, Theme, createStyles } from "@material-ui/core/styles"; 2 | 3 | export default makeStyles((theme: Theme) => 4 | createStyles({ 5 | root: { 6 | margin: theme.spacing(5, 0, 10), 7 | padding: theme.spacing(0, 4), 8 | display: "flex", 9 | flexDirection: "row", 10 | alignItems: "center", 11 | justifyContent: "space-between", 12 | borderRadius: 36, 13 | height: 72, 14 | [theme.breakpoints.down("xs")]: { 15 | padding: theme.spacing(0, 1), 16 | }, 17 | }, 18 | arrowButton: { 19 | borderRadius: 36, 20 | fontFamily: "OpenSans", 21 | fontWeight: 600, 22 | }, 23 | }) 24 | ); 25 | -------------------------------------------------------------------------------- /src/images/bookmark-add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 / basic / bookmark-add 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/styles/pages/Drafts.styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, Theme, createStyles } from "@material-ui/core/styles"; 2 | import { getThemeType } from "utils/createTheme"; 3 | 4 | export default makeStyles((theme: Theme) => 5 | createStyles({ 6 | root: { 7 | display: "flex", 8 | flexDirection: "column", 9 | }, 10 | title: { 11 | fontFamily: "OpenSans", 12 | fontSize: 40, 13 | marginBottom: theme.spacing(4), 14 | color: 15 | getThemeType(theme.palette.background.default) === "dark" 16 | ? "#FFF" 17 | : theme.palette.text.primary, 18 | }, 19 | center: { 20 | height: "50vh", 21 | width: "100%", 22 | display: "flex", 23 | justifyContent: "center", 24 | alignItems: "center", 25 | }, 26 | }) 27 | ); 28 | -------------------------------------------------------------------------------- /src/images/bookmarks.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 / basic / bookmarks 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/utils/createThread.ts: -------------------------------------------------------------------------------- 1 | import Box from "3box"; 2 | import getSpaceName from "utils/getSpaceName"; 3 | 4 | export default async (domain: string, address?: string) => { 5 | const spaceName = getSpaceName(domain); 6 | if (!address) { 7 | address = (await (window as any).ethereum.enable())[0]; 8 | } 9 | 10 | const provider = await Box.get3idConnectProvider(); 11 | const box = await Box.create(provider); 12 | await box.auth([spaceName], { address }); 13 | await box.syncDone; 14 | 15 | const space = await box.openSpace(spaceName); 16 | await space.syncDone; 17 | 18 | const thread = await space.joinThread("blog-posts", { 19 | members: true, 20 | }); 21 | return { 22 | adminWallet: address as string, 23 | threadAddress: thread.address as string, 24 | spaceName, 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /src/types/actions.ts: -------------------------------------------------------------------------------- 1 | import { BlogPost } from "./app"; 2 | 3 | export const SET_CONFIG = "SET_CONFIG"; 4 | export const UPDATE_AUTH = "UPDATE_AUTH"; 5 | export const LOG_IN = "LOG_IN"; 6 | export const LOG_OUT = "LOG_OUT"; 7 | export const ADD_POST = "ADD_POST"; 8 | export const SET_POSTS = "SET_POSTS"; 9 | export const DELETE_POST = "DELETE_POST"; 10 | export const SET_MODERATORS = "SET_MODERATORS"; 11 | export const SET_MODERATOR_NAMES = "SET_MODERATOR_NAMES"; 12 | 13 | export type ActionTypes = 14 | | typeof SET_CONFIG 15 | | typeof UPDATE_AUTH 16 | | typeof LOG_IN 17 | | typeof LOG_OUT 18 | | typeof ADD_POST 19 | | typeof SET_POSTS 20 | | typeof DELETE_POST 21 | | typeof SET_MODERATORS 22 | | typeof SET_MODERATOR_NAMES; 23 | 24 | export interface AppAction { 25 | type: ActionTypes; 26 | value?: BlogPost | any; // todo: improve 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/socialShare.ts: -------------------------------------------------------------------------------- 1 | export const handleFacebook = async () => { 2 | window.open( 3 | "https://www.facebook.com/sharer/sharer.php?u=" + 4 | encodeURIComponent(document.URL) + 5 | "&t=" + 6 | encodeURIComponent(document.URL), 7 | "_blank" 8 | ); 9 | }; 10 | export const handleTwitter = async () => { 11 | window.open( 12 | "https://twitter.com/intent/tweet?text=%20Check%20out%20this%20awesome%20content%20-%20" + 13 | encodeURIComponent(document.title) + 14 | ":%20 " + 15 | encodeURIComponent(document.URL), 16 | "_blank" 17 | ); 18 | }; 19 | export const handleLinkedIn = async () => { 20 | window.open( 21 | "http://www.linkedin.com/shareArticle?mini=true&url=" + 22 | encodeURIComponent(document.URL) + 23 | "&title=" + 24 | encodeURIComponent(document.title), 25 | "_blank" 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /website-builder: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function error() { 4 | echo "Error: $1" 5 | exit 2 6 | } 7 | 8 | commit_hash=$(git log -n1 --format=format:"%H") 9 | production=0 10 | while [[ $# -gt 0 ]]; do 11 | key="$1" 12 | case "$key" in 13 | -p | --production) 14 | production=1 15 | ;; 16 | -d | --dirty) 17 | dirty=1 18 | ;; 19 | *) 20 | error "unknown option '$key'" 21 | ;; 22 | esac 23 | shift 24 | done 25 | 26 | git remote update >/dev/null 2>&1 27 | diff=`git diff HEAD` 28 | if [[ -z $dirty && $production -eq 1 && ( -n $diff || $commit_hash != $(git rev-parse @{u}))]]; then 29 | error 'push all code before deploy' 30 | fi 31 | 32 | yarn build 33 | echo {\"build\" : \"$commit_hash\"} > build/version.json 34 | 35 | if [[ $production -eq 1 ]]; then 36 | rm build/config.json build/logo.png build/manifest.json 37 | fi -------------------------------------------------------------------------------- /src/images/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 / logos / facebook 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/images/medium.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 / logos / medium 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/images/logout-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 / basic / logout-circle 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/images/file-draft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 / file / file-draft 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "reseller-test-don0.crypto", 3 | "title": "Don's Blog", 4 | "logo": "logo.png", 5 | "threadAddress": "/orbitdb/zdpuAsWFwTvGFTQsKpjF48jGjsBeDLrtdpGvaMxhFYvaZHNvv/3box.thread.draft-blog-reseller-test-don0#crypto.blog-posts", 6 | "adminWallet": "0x47b0FdE4622577cb10ED2c79108D79CBb582eE5C", 7 | "spaceName": "draft-blog-reseller-test-don0#crypto", 8 | "theme": { 9 | "primary": "#022120", 10 | "secondary": "#107F62", 11 | "background": "#FBF7ED" 12 | }, 13 | "socials": { 14 | "hasWebsite": true, 15 | "hasFacebook": true, 16 | "hasInstagram": true, 17 | "hasLinkedIn": true, 18 | "hasMedium": true, 19 | "hasTelegram": true, 20 | "hasTwitter": true, 21 | "hasYouTube": true, 22 | "website": "donstolz.tech", 23 | "facebook": "facebook.com", 24 | "instagram": "instagram.com", 25 | "linkedIn": "linkedIn.com", 26 | "medium": "medium.com", 27 | "telegram": "telegram.com", 28 | "twitter": "twitter.com", 29 | "youTube": "youtube.com" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/parseMessage.ts: -------------------------------------------------------------------------------- 1 | import { ThreadObject, BlogPost } from "types/app"; 2 | import fm from "front-matter"; 3 | 4 | export default ( 5 | postThread: ThreadObject, 6 | moderatorNames?: { [key: string]: string } 7 | ): BlogPost => { 8 | try { 9 | const parsedMessage: any = fm(postThread.message); 10 | const author = moderatorNames 11 | ? moderatorNames[postThread.author] 12 | : postThread.author; 13 | 14 | const tags = parsedMessage.attributes.tags 15 | ? parsedMessage.attributes.tags.split(",") 16 | : []; 17 | return { 18 | title: parsedMessage.attributes.title, 19 | description: parsedMessage.attributes.description, 20 | body: parsedMessage.body, 21 | threadData: { ...postThread, author }, 22 | tags, 23 | }; 24 | } catch (error) { 25 | console.error(error); 26 | return { 27 | title: "Error Parsing message", 28 | description: "", 29 | body: postThread.message, 30 | threadData: { ...postThread, author: "author" }, 31 | tags: [], 32 | }; 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/images/linkedin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 / logos / linkedin 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Init.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import CircularProgress from "@material-ui/core/CircularProgress"; 3 | import useStyles from "styles/App.styles"; 4 | import useAsyncEffect from "use-async-effect"; 5 | import createThread from "utils/createThread"; 6 | 7 | const App: React.FunctionComponent = () => { 8 | const classes = useStyles(); 9 | const [config, setConfig] = React.useState({ 10 | threadAddress: "", 11 | adminWallet: "", 12 | spaceName: "", 13 | }); 14 | const [loading, setLoading] = React.useState(true); 15 | 16 | useAsyncEffect(async () => { 17 | const newConfig = await createThread("reseller-test-don1.crypto"); 18 | setConfig(newConfig); 19 | setLoading(false); 20 | }, []); 21 | 22 | return ( 23 |
24 | {loading ? ( 25 | 26 | ) : ( 27 |
28 |

adminWallet: {config.adminWallet}

29 |

threadAddress: {config.threadAddress}

30 |

spaceName: {config.spaceName}

31 |
32 | )} 33 |
34 | ); 35 | }; 36 | 37 | export default App; 38 | -------------------------------------------------------------------------------- /src/images/instagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 / logos / instagram 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/Editor.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import MarkDownEditor from "react-mde"; 3 | import Showdown from "showdown"; 4 | import "react-mde/lib/styles/css/react-mde-all.css"; 5 | import useStyles from "styles/components/Editor.styles"; 6 | import { showdownOptions } from "config/showdown"; 7 | 8 | const converter = new Showdown.Converter(showdownOptions); 9 | 10 | interface Props { 11 | value: string; 12 | onChange: (value: string) => void; 13 | } 14 | 15 | const Editor: React.FunctionComponent = ({ value, onChange }) => { 16 | const [selectedTab, setSelectedTab] = React.useState< 17 | "write" | "preview" | undefined 18 | >("write"); 19 | const classes = useStyles(); 20 | // Source: https://github.com/andrerpena/react-mde 21 | return ( 22 | 32 | Promise.resolve(converter.makeHtml(markdown)) 33 | } 34 | /> 35 | ); 36 | }; 37 | 38 | export default Editor; 39 | -------------------------------------------------------------------------------- /src/images/trash-empty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 / basic / trash-empty 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/styles/components/Footer.styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, createStyles, Theme } from "@material-ui/core/styles"; 2 | 3 | export default makeStyles((theme: Theme) => 4 | createStyles({ 5 | footer: { 6 | backgroundColor: theme.palette.primary.main, 7 | padding: theme.spacing(4, 2), 8 | display: "flex", 9 | flexDirection: "column", 10 | alignItems: "center", 11 | justifyContent: "center", 12 | marginBottom: theme.spacing(-2.5), 13 | [theme.breakpoints.up("sm")]: { 14 | flexDirection: "row-reverse", 15 | justifyContent: "space-between", 16 | padding: theme.spacing(5, 10, 9), 17 | }, 18 | }, 19 | logosContainer: { 20 | display: "flex", 21 | alignItems: "center", 22 | marginBottom: theme.spacing(2), 23 | }, 24 | unstoppableLogo: { 25 | marginRight: theme.spacing(7), 26 | }, 27 | creditContainer: {}, 28 | creditText: { 29 | color: theme.palette.primary.contrastText, 30 | fontFamily: "OpenSans", 31 | fontSize: 14, 32 | }, 33 | hover: { 34 | "&:hover": { 35 | cursor: "pointer !important", 36 | textDecoration: "underline", 37 | }, 38 | }, 39 | }) 40 | ); 41 | -------------------------------------------------------------------------------- /src/styles/components/WalletModal.styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, Theme, createStyles } from "@material-ui/core/styles"; 2 | 3 | export default makeStyles((theme: Theme) => 4 | createStyles({ 5 | backdrop: { 6 | zIndex: theme.zIndex.drawer + 1, 7 | color: theme.palette.common.white, 8 | }, 9 | paper: { 10 | margin: 0, 11 | }, 12 | modalContent: { 13 | backgroundColor: "rgba(17, 51, 83, 0.02)", 14 | padding: theme.spacing(2), 15 | display: "flex", 16 | flexDirection: "column", 17 | minWidth: 600, 18 | }, 19 | modalHeader: { 20 | width: "100%", 21 | display: "flex", 22 | alignItems: "flex-start", 23 | justifyContent: "space-between", 24 | }, 25 | modalTitle: { 26 | fontWeight: "bold", 27 | marginLeft: theme.spacing(1), 28 | marginTop: theme.spacing(1), 29 | }, 30 | modalCloseIcon: { 31 | padding: 0, 32 | }, 33 | providerColumn: { 34 | display: "flex", 35 | flexDirection: "column", 36 | }, 37 | providerButton: { 38 | fontWeight: "bold", 39 | fontSize: 16, 40 | height: 56, 41 | textTransform: "none", 42 | marginTop: theme.spacing(2), 43 | borderRadius: 6, 44 | }, 45 | }) 46 | ); 47 | -------------------------------------------------------------------------------- /src/images/cloud-upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 / software / cloud-upload 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/utils/useQueryStringParams.ts: -------------------------------------------------------------------------------- 1 | import qs from "query-string"; 2 | import { useState, useCallback } from "react"; 3 | 4 | const setQueryStringWithoutPageReload = (qsValue: string) => { 5 | const newurl = 6 | window.location.protocol + 7 | "//" + 8 | window.location.host + 9 | window.location.pathname + 10 | qsValue; 11 | 12 | window.history.pushState({ path: newurl }, "", newurl); 13 | }; 14 | 15 | const setQueryStringValue = ( 16 | key: string, 17 | value: string | string[], 18 | queryString = window.location.search 19 | ) => { 20 | const values = qs.parse(queryString); 21 | const newQsValue = qs.stringify({ ...values, [key]: value }); 22 | setQueryStringWithoutPageReload(`?${newQsValue}`); 23 | }; 24 | 25 | const getQueryStringValue = ( 26 | key: string, 27 | queryString = window.location.search 28 | ) => { 29 | const values = qs.parse(queryString); 30 | return values[key]; 31 | }; 32 | 33 | const useQueryString = (key: string, initialValue?: string | string[]) => { 34 | const [value, setValue] = useState(getQueryStringValue(key) || initialValue); 35 | const onSetValue = useCallback( 36 | (newValue: string | string[]) => { 37 | setValue(newValue); 38 | setQueryStringValue(key, newValue); 39 | }, 40 | [key] 41 | ); 42 | 43 | return { value, onSetValue }; 44 | }; 45 | 46 | export default useQueryString; 47 | -------------------------------------------------------------------------------- /src/images/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 / editing / user 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 17 | 26 | ${title.value} 27 | 28 | 29 | 30 | 31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /src/images/telegram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 / logos / telegram 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/styles/index.styles.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Merriweather"; 3 | src: local("Merriweather"), 4 | url(../fonts/Merriweather-Regular.ttf) format("truetype"); 5 | font-weight: normal; 6 | font-style: normal; 7 | } 8 | 9 | @font-face { 10 | font-family: "Merriweather"; 11 | src: local("Merriweather"), 12 | url(../fonts/Merriweather-Bold.ttf) format("truetype"); 13 | font-weight: bold; 14 | font-style: normal; 15 | } 16 | 17 | @font-face { 18 | font-family: "Merriweather"; 19 | src: local("Merriweather"), 20 | url(../fonts/Merriweather-Black.ttf) format("truetype"); 21 | font-weight: 900; 22 | font-style: normal; 23 | } 24 | 25 | @font-face { 26 | font-family: "OpenSans"; 27 | src: local("OpenSans"), url(../fonts/OpenSans-Regular.ttf) format("truetype"); 28 | font-weight: normal; 29 | font-style: normal; 30 | } 31 | 32 | @font-face { 33 | font-family: "OpenSans"; 34 | src: local("OpenSans"), url(../fonts/OpenSans-Bold.ttf) format("truetype"); 35 | font-weight: bold; 36 | font-style: normal; 37 | } 38 | 39 | @font-face { 40 | font-family: "OpenSans"; 41 | src: local("OpenSans"), url(../fonts/OpenSans-SemiBold.ttf) format("truetype"); 42 | font-weight: semi-bold; 43 | font-style: normal; 44 | } 45 | 46 | img { 47 | max-width: 100%; 48 | } 49 | 50 | iframe { 51 | z-index: 1100 !important; 52 | } 53 | 54 | /* 3Box Comments */ 55 | .comments_footer { 56 | overflow-x: hidden !important; 57 | } 58 | -------------------------------------------------------------------------------- /src/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PostPreview from "components/PostPreview"; 3 | import CircularProgress from "@material-ui/core/CircularProgress"; 4 | import { BlogPost, RoutingProps } from "types/app"; 5 | import { getPosts } from "services/blogActions"; 6 | import appContext from "services/appContext"; 7 | import useStyles from "styles/pages/Home.styles"; 8 | import useAsyncEffect from "use-async-effect"; 9 | 10 | const Home: React.FunctionComponent = ({ handleRoute }) => { 11 | const classes = useStyles(); 12 | const [loading, setLoading] = React.useState(true); 13 | const [posts, setPosts] = React.useState([]); 14 | const { state, dispatch } = React.useContext(appContext); 15 | 16 | useAsyncEffect(async () => { 17 | if (!state.posts) { 18 | setLoading(true); 19 | const _posts = await getPosts({ state, dispatch })(); 20 | setPosts(_posts); 21 | setLoading(false); 22 | } else { 23 | setPosts(state.posts); 24 | setLoading(false); 25 | } 26 | }, [state.posts]); 27 | 28 | return ( 29 |
30 | {loading ? ( 31 |
32 | 33 |
34 | ) : ( 35 | posts.map((post: BlogPost, index: number) => ( 36 | 37 | )) 38 | )} 39 |
40 | ); 41 | }; 42 | 43 | export default Home; 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "3box-blog", 3 | "version": "4.0.0", 4 | "homepage": "./", 5 | "license": "MIT", 6 | "dependencies": { 7 | "3box": "^1.21.0", 8 | "3box-comments-react": "^3.0.5", 9 | "@material-ui/core": "^4.10.2", 10 | "@material-ui/icons": "^4.9.1", 11 | "@types/react": "^16.9.41", 12 | "@types/react-dom": "^16.9.8", 13 | "@walletconnect/web3-provider": "^1.0.11", 14 | "front-matter": "^4.0.2", 15 | "query-string": "^6.13.1", 16 | "react": "^16.13.1", 17 | "react-dom": "^16.13.1", 18 | "react-mde": "^10.1.0", 19 | "react-scripts": "^3.4.1", 20 | "react-showdown": "^2.1.0", 21 | "showdown": "^1.9.1", 22 | "typescript": "^3.9.5", 23 | "use-async-effect": "^2.2.2" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "build:template": "./website-builder -p", 29 | "build:serve": "yarn build && yarn serve", 30 | "serve": "serve -s build", 31 | "test": "react-scripts test", 32 | "eject": "react-scripts eject" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "devDependencies": { 47 | "@types/react-router-dom": "^5.1.5", 48 | "@types/showdown": "^1.9.3", 49 | "serve": "^11.3.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/images/pencil-create.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 / editing / pencil-create 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/utils/createTheme.ts: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from "@material-ui/core/styles"; 2 | 3 | export const getThemeType = (color: string) => 4 | parseInt(color.replace("#", ""), 16) > 0xffffff / 2 ? "light" : "dark"; 5 | 6 | const createTheme = (primary: string, secondary: string, background: string) => 7 | createMuiTheme({ 8 | typography: { 9 | fontFamily: "Merriweather", 10 | }, 11 | palette: { 12 | type: "light", 13 | primary: { 14 | main: primary, 15 | }, 16 | secondary: { 17 | main: secondary, 18 | }, 19 | error: { 20 | main: "#d61e1e", 21 | }, 22 | background: { 23 | default: background, 24 | }, 25 | }, 26 | overrides: { 27 | MuiButton: { 28 | contained: { 29 | fontFamily: "OpenSans", 30 | fontSize: 16, 31 | fontWeight: "bold", 32 | borderRadius: 20, 33 | height: 40, 34 | }, 35 | }, 36 | MuiOutlinedInput: { 37 | root: { 38 | borderRadius: 0, 39 | }, 40 | }, 41 | MuiListItemIcon: { 42 | root: { 43 | minWidth: 40, 44 | }, 45 | }, 46 | }, 47 | }); 48 | 49 | export const defaultTheme = createMuiTheme({ 50 | palette: { 51 | primary: { 52 | main: "#556cd6", 53 | }, 54 | secondary: { 55 | main: "#19857b", 56 | }, 57 | error: { 58 | main: "#d61e1e", 59 | }, 60 | background: { 61 | default: "#fff", 62 | }, 63 | }, 64 | }); 65 | 66 | export default createTheme; 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 3Box React Blog 2 | 3 | A React & Typescript blog that uses 3Box decentralized storage and authentication 4 | 5 | ## How to use 6 | 7 | ```sh 8 | yarn 9 | yarn start 10 | ``` 11 | 12 | This will run the website with whatever blog is specified in `public/config.json`. 13 | 14 | To create a new blog you will have run `src/utils/createThread.ts` and then update `public/config.json` with the new _domain, threadAddress, spaceName, & adminWallet_. 15 | 16 | ## How It Works 17 | 18 | **Make sure the [`blogConfig.json`](public/config.json) is set up properly** 19 | 20 | The app use's React [`useContext`](src/services/appContext.ts) & [`useReducer`](src/services/appReducer.ts) function to maintain a global app state. 21 | 22 | ```ts 23 | // State: 24 | interface AppState { 25 | box: any; 26 | space: any; 27 | thread: any; 28 | user: { 29 | loggedIn: boolean; 30 | walletAddress?: string; 31 | profileImg?: string; 32 | }; 33 | posts?: BlogPost[]; 34 | } 35 | 36 | // Actions: 37 | type ActionTypes = 38 | | typeof ADD_BOX 39 | | typeof LOG_IN 40 | | typeof LOG_OUT 41 | | typeof ADD_POST 42 | | typeof SET_POSTS 43 | | typeof DELETE_POST; 44 | ``` 45 | 46 | ## Libraries 47 | 48 | - [Material UI - React & Typescript](https://github.com/mui-org/material-ui/tree/master/examples/create-react-app-with-typescript) 49 | 50 | - [3Box](https://docs.3box.io/api/index) 51 | - [auth](https://docs.3box.io/api/auth) 52 | - [profiles](https://docs.3box.io/api/profiles/get) 53 | - [threads](https://docs.3box.io/api/messaging) 54 | - [3box-comments-react](https://github.com/3box/3box-comments-react) 55 | -------------------------------------------------------------------------------- /src/styles/components/LikeShare.styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, Theme, createStyles } from "@material-ui/core/styles"; 2 | 3 | export default makeStyles((theme: Theme) => 4 | createStyles({ 5 | container: { 6 | display: "flex", 7 | flexDirection: "row", 8 | alignItems: "center", 9 | justifyContent: "space-between", 10 | // TODO: Put back when adding comments component 11 | // marginBottom: theme.spacing(3), 12 | [theme.breakpoints.down("xs")]: { 13 | flexDirection: "column", 14 | }, 15 | }, 16 | leftButtons: {}, 17 | likeButton: { 18 | borderRadius: 20, 19 | fontFamily: "OpenSans", 20 | fontSize: 16, 21 | height: 40, 22 | [theme.breakpoints.down("xs")]: { 23 | marginBottom: theme.spacing(2), 24 | }, 25 | }, 26 | shareRow: { 27 | display: "flex", 28 | flexDirection: "row", 29 | alignItems: "center", 30 | justifyContent: "space-between", 31 | [theme.breakpoints.down("xs")]: { 32 | alignItems: "flex-start", 33 | flexDirection: "column", 34 | }, 35 | }, 36 | shareText: { 37 | fontFamily: "OpenSans", 38 | fontSize: 16, 39 | fontWeight: "bold", 40 | marginRight: theme.spacing(1), 41 | [theme.breakpoints.down("xs")]: { 42 | marginTop: theme.spacing(2), 43 | }, 44 | }, 45 | icon: {}, 46 | iconText: { 47 | textTransform: "none", 48 | marginLeft: theme.spacing(1), 49 | fontFamily: "OpenSans", 50 | fontSize: 16, 51 | color: theme.palette.secondary.main, 52 | }, 53 | }) 54 | ); 55 | -------------------------------------------------------------------------------- /src/styles/components/SharePopup.styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, Theme, createStyles } from "@material-ui/core/styles"; 2 | 3 | export default makeStyles((theme: Theme) => 4 | createStyles({ 5 | root: { 6 | display: "flex", 7 | flexDirection: "row", 8 | alignItems: "center", 9 | justifyContent: "space-around", 10 | padding: theme.spacing(4), 11 | [theme.breakpoints.down("xs")]: { 12 | flexDirection: "column", 13 | padding: theme.spacing(2), 14 | }, 15 | }, 16 | shareText: { 17 | fontFamily: "OpenSans", 18 | fontWeight: "bold", 19 | fontSize: 24, 20 | maxWidth: 122, 21 | }, 22 | facebook: { 23 | backgroundColor: "#4469b0", 24 | marginLeft: theme.spacing(2), 25 | "&:hover": { 26 | backgroundColor: "#4469b080 !important", 27 | }, 28 | [theme.breakpoints.down("xs")]: { 29 | marginLeft: 0, 30 | marginBottom: theme.spacing(1.5), 31 | }, 32 | }, 33 | twitter: { 34 | backgroundColor: "#2aa3ef", 35 | marginLeft: theme.spacing(2), 36 | "&:hover": { 37 | backgroundColor: "#2aa3ef80 !important", 38 | }, 39 | [theme.breakpoints.down("xs")]: { 40 | marginLeft: 0, 41 | marginBottom: theme.spacing(1.5), 42 | }, 43 | }, 44 | linkedIn: { 45 | backgroundColor: "#1178b3", 46 | marginLeft: theme.spacing(2), 47 | "&:hover": { 48 | backgroundColor: "#1178b380 !important", 49 | }, 50 | [theme.breakpoints.down("xs")]: { 51 | marginLeft: 0, 52 | marginBottom: theme.spacing(1.5), 53 | }, 54 | }, 55 | }) 56 | ); 57 | -------------------------------------------------------------------------------- /src/images/pencil-edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 / editing / pencil-edit 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/styles/pages/Read.styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, Theme, createStyles } from "@material-ui/core/styles"; 2 | 3 | export default makeStyles((theme: Theme) => 4 | createStyles({ 5 | root: { 6 | display: "flex", 7 | flexDirection: "column", 8 | padding: theme.spacing(4), 9 | margin: theme.spacing(0, 2, 10), 10 | [theme.breakpoints.down("xs")]: { 11 | margin: theme.spacing(0, 0, 10), 12 | }, 13 | }, 14 | headerRow: { 15 | display: "flex", 16 | justifyContent: "space-between", 17 | marginBottom: theme.spacing(2), 18 | }, 19 | title: { 20 | wordBreak: "break-word", 21 | [theme.breakpoints.down("xs")]: { 22 | fontSize: "3rem", 23 | }, 24 | }, 25 | dateAuthorRow: {}, 26 | caption: { 27 | fontFamily: "OpenSans", 28 | fontSize: 14, 29 | wordBreak: "break-word", 30 | }, 31 | description: { 32 | marginBottom: theme.spacing(4), 33 | }, 34 | bodyContainer: { 35 | display: "flex", 36 | marginBottom: theme.spacing(4), 37 | maxWidth: "100%", 38 | }, 39 | divider: { 40 | margin: theme.spacing(3, 0), 41 | }, 42 | commentContainer: { 43 | display: "flex", 44 | flex: 1, 45 | paddingTop: theme.spacing(9), 46 | margin: theme.spacing(0, -4, -4), 47 | background: "#f7f7f7", 48 | borderBottomLeftRadius: 4, 49 | borderBottomRightRadius: 4, 50 | }, 51 | comments: { 52 | marginRight: "auto", 53 | marginLeft: "auto", 54 | maxWidth: 616, 55 | width: "100%", 56 | padding: theme.spacing(0, 1.5), 57 | }, 58 | loadingContainer: { 59 | display: "flex", 60 | alignItems: "center", 61 | justifyContent: "center", 62 | minHeight: "60vh", 63 | }, 64 | }) 65 | ); 66 | -------------------------------------------------------------------------------- /src/styles/components/PostPreview.styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, Theme, createStyles } from "@material-ui/core/styles"; 2 | 3 | export default makeStyles((theme: Theme) => 4 | createStyles({ 5 | container: { 6 | display: "flex", 7 | flexDirection: "column", 8 | marginBottom: theme.spacing(1), 9 | padding: theme.spacing(4), 10 | maxWidth: 960, 11 | }, 12 | topRow: { 13 | display: "flex", 14 | alignItems: "center", 15 | justifyContent: "space-between", 16 | marginBottom: theme.spacing(1), 17 | }, 18 | title: { 19 | fontSize: 32, 20 | fontWeight: "bold", 21 | wordWrap: "break-word", 22 | }, 23 | dateAuthorRow: { 24 | display: "flex", 25 | alignItems: "center", 26 | marginBottom: theme.spacing(3), 27 | }, 28 | caption: { 29 | fontFamily: "OpenSans", 30 | fontSize: 14, 31 | wordBreak: "break-word", 32 | }, 33 | description: {}, 34 | buttonRow: { 35 | display: "flex", 36 | flexGrow: 1, 37 | alignItems: "center", 38 | justifyContent: "space-between", 39 | marginTop: theme.spacing(4), 40 | [theme.breakpoints.down("xs")]: { 41 | flexDirection: "column", 42 | width: "100%", 43 | }, 44 | }, 45 | containedButton: { 46 | display: "flex", 47 | alignItems: "center", 48 | width: 158, 49 | borderRadius: 25, 50 | fontSize: 16, 51 | [theme.breakpoints.down("xs")]: { 52 | width: "100%", 53 | }, 54 | }, 55 | destroyButton: { 56 | fontFamily: "OpenSans", 57 | fontWeight: 600, 58 | color: theme.palette.error.main, 59 | borderRadius: 25, 60 | fontSize: 16, 61 | paddingRight: theme.spacing(1), 62 | paddingLeft: theme.spacing(1), 63 | [theme.breakpoints.down("xs")]: { 64 | width: "100%", 65 | marginTop: theme.spacing(2), 66 | }, 67 | }, 68 | }) 69 | ); 70 | -------------------------------------------------------------------------------- /src/pages/Drafts.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PostPreview from "components/PostPreview"; 3 | import CircularProgress from "@material-ui/core/CircularProgress"; 4 | import { BlogPost, RoutingProps } from "types/app"; 5 | import { getDrafts, removeDraft } from "services/blogActions"; 6 | import appContext from "services/appContext"; 7 | import useStyles from "styles/pages/Drafts.styles"; 8 | import useAsyncEffect from "use-async-effect"; 9 | import Typography from "@material-ui/core/Typography"; 10 | 11 | const Drafts: React.FunctionComponent = ({ handleRoute }) => { 12 | const classes = useStyles(); 13 | const [loading, setLoading] = React.useState(true); 14 | const [drafts, setDrafts] = React.useState([]); 15 | const { state, dispatch } = React.useContext(appContext); 16 | const { loggedIn } = state.user; 17 | 18 | React.useEffect(() => { 19 | if (!loggedIn) { 20 | handleRoute(""); 21 | } 22 | }, [loggedIn, handleRoute]); 23 | 24 | useAsyncEffect(async () => { 25 | if (loggedIn) { 26 | const newDrafts = await getDrafts({ state, dispatch })(); 27 | setDrafts(newDrafts); 28 | setLoading(false); 29 | } 30 | }, []); 31 | 32 | const handleRemoveDraft = async (draftId: string) => { 33 | await removeDraft({ state, dispatch })(draftId); 34 | }; 35 | 36 | return ( 37 |
38 | 39 | Drafts 40 | 41 | {loading ? ( 42 |
43 | 44 |
45 | ) : ( 46 | drafts.map((draft: BlogPost, index: number) => ( 47 | 54 | )) 55 | )} 56 |
57 | ); 58 | }; 59 | 60 | export default Drafts; 61 | -------------------------------------------------------------------------------- /src/components/SharePopup.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useStyles from "styles/components/SharePopup.styles"; 3 | import IconButton from "@material-ui/core/IconButton"; 4 | import CustomIcon from "./CustomIcon"; 5 | import Paper from "@material-ui/core/Paper"; 6 | import Typography from "@material-ui/core/Typography"; 7 | import { 8 | handleFacebook, 9 | handleTwitter, 10 | handleLinkedIn, 11 | } from "utils/socialShare"; 12 | import ClickAwayListener from "@material-ui/core/ClickAwayListener"; 13 | import Popover from "@material-ui/core/Popover"; 14 | 15 | interface Props { 16 | handleClose: () => void; 17 | anchorRef: React.RefObject; 18 | open: boolean; 19 | } 20 | 21 | const SharePopup: React.FunctionComponent = ({ 22 | handleClose, 23 | anchorRef, 24 | open, 25 | }) => { 26 | const classes = useStyles(); 27 | 28 | return ( 29 | 41 | 42 | 43 | 44 | Share this post on 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | }; 60 | 61 | export default SharePopup; 62 | -------------------------------------------------------------------------------- /src/pages/Bookmarks.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PostPreview from "components/PostPreview"; 3 | import CircularProgress from "@material-ui/core/CircularProgress"; 4 | import { BlogPost, RoutingProps } from "types/app"; 5 | import appContext from "services/appContext"; 6 | import useStyles from "styles/pages/Drafts.styles"; 7 | import useAsyncEffect from "use-async-effect"; 8 | import Typography from "@material-ui/core/Typography"; 9 | import { getBookmarks } from "services/userActions"; 10 | 11 | const Bookmarks: React.FunctionComponent = ({ handleRoute }) => { 12 | const classes = useStyles(); 13 | const [loading, setLoading] = React.useState(true); 14 | const [bookmarks, setBookmarks] = React.useState([]); 15 | const { state, dispatch } = React.useContext(appContext); 16 | const { loggedIn } = state.user; 17 | 18 | React.useEffect(() => { 19 | if (!loggedIn) { 20 | handleRoute(""); 21 | } 22 | }, [loggedIn, handleRoute]); 23 | 24 | useAsyncEffect(async () => { 25 | if (loggedIn) { 26 | const initialBookmarks = await getBookmarks({ state, dispatch })(); 27 | setBookmarks(initialBookmarks); 28 | setLoading(false); 29 | } 30 | }, []); 31 | 32 | const handleRemove = (postId: string) => { 33 | const newBookmarks = bookmarks.filter( 34 | (bookmark) => bookmark.threadData?.postId !== postId 35 | ); 36 | setBookmarks(newBookmarks); 37 | }; 38 | 39 | return ( 40 |
41 | 42 | Bookmarks 43 | 44 | {loading && ( 45 |
46 | 47 |
48 | )} 49 | {bookmarks && 50 | bookmarks.map((bookmark: BlogPost, index: number) => ( 51 | 57 | ))} 58 |
59 | ); 60 | }; 61 | 62 | export default Bookmarks; 63 | -------------------------------------------------------------------------------- /src/services/appReducer.ts: -------------------------------------------------------------------------------- 1 | import { AppState } from "types/app"; 2 | import { 3 | AppAction, 4 | LOG_IN, 5 | LOG_OUT, 6 | ADD_POST, 7 | SET_POSTS, 8 | DELETE_POST, 9 | SET_CONFIG, 10 | SET_MODERATORS, 11 | SET_MODERATOR_NAMES, 12 | UPDATE_AUTH, 13 | } from "types/actions"; 14 | 15 | const appReducer = (state: AppState, action: AppAction) => { 16 | switch (action.type) { 17 | case SET_CONFIG: { 18 | return { ...state, ...action.value } as AppState; 19 | } 20 | case UPDATE_AUTH: { 21 | return { ...state, user: { ...state.user, loading: true } } as AppState; 22 | } 23 | case LOG_IN: { 24 | const { user, thread, space, box, moderators } = action.value; 25 | return { ...state, user, thread, space, box, moderators } as AppState; 26 | } 27 | case LOG_OUT: { 28 | return { 29 | ...state, 30 | user: { 31 | loggedIn: false, 32 | loading: false, 33 | }, 34 | thread: null, 35 | space: null, 36 | box: null, 37 | } as AppState; 38 | } 39 | case ADD_POST: { 40 | const posts = state.posts || []; 41 | posts.unshift(action.value.post); 42 | return { ...state, posts } as AppState; 43 | } 44 | case SET_POSTS: { 45 | return { ...state, posts: action.value.posts } as AppState; 46 | } 47 | case DELETE_POST: { 48 | const { postId } = action.value; 49 | const index = state.posts?.findIndex( 50 | (post) => post.threadData?.postId === postId 51 | ); 52 | let posts = state.posts ? [...state.posts] : []; 53 | if (typeof index !== "undefined" && index > -1) { 54 | posts.splice(index, 1); 55 | } 56 | 57 | return { ...state, posts } as AppState; 58 | } 59 | case SET_MODERATORS: { 60 | const { moderators } = action.value; 61 | return { ...state, moderators } as AppState; 62 | } 63 | case SET_MODERATOR_NAMES: { 64 | const { moderatorNames } = action.value; 65 | return { ...state, moderatorNames } as AppState; 66 | } 67 | default: 68 | throw new Error(); 69 | } 70 | }; 71 | 72 | export default appReducer; 73 | -------------------------------------------------------------------------------- /src/images/thumbs-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 / basic / thumbs-up 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/WalletModal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Typography from "@material-ui/core/Typography"; 3 | import useStyles from "styles/components/WalletModal.styles"; 4 | import Backdrop from "@material-ui/core/Backdrop"; 5 | import Paper from "@material-ui/core/Paper"; 6 | import IconButton from "@material-ui/core/IconButton"; 7 | import CloseIcon from "@material-ui/icons/Close"; 8 | import Check from "@material-ui/icons/Check"; 9 | import Button from "@material-ui/core/Button"; 10 | import { WALLET_TYPE } from "types/app"; 11 | 12 | interface Props { 13 | open: boolean; 14 | onClose: () => void; 15 | onSelectWallet: (wallet: WALLET_TYPE) => void; 16 | } 17 | 18 | const WalletModal: React.FunctionComponent = ({ 19 | open, 20 | onClose, 21 | onSelectWallet, 22 | }) => { 23 | const classes = useStyles(); 24 | const onSelectWeb3 = () => { 25 | onSelectWallet("web3"); 26 | }; 27 | const onSelectWalletConnect = () => { 28 | onSelectWallet("walletConnect"); 29 | }; 30 | 31 | return ( 32 | 33 | 34 |
35 |
36 | 37 | Select Wallet Access Provider 38 | 39 | 40 | 41 | 42 |
43 |
44 | 53 | 62 |
63 |
64 |
65 |
66 | ); 67 | }; 68 | 69 | export default WalletModal; 70 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Typography from "@material-ui/core/Typography"; 3 | import useStyles from "styles/components/Footer.styles"; 4 | import appContext from "services/appContext"; 5 | import UnstoppableDark from "images/unstoppable-domains-logo-dark.svg"; 6 | import UnstoppableLight from "images/unstoppable-domains-logo-light.svg"; 7 | import ThreeBoxDark from "images/three-box-logo-dark.svg"; 8 | import ThreeBoxLight from "images/three-box-logo-light.svg"; 9 | import { getThemeType } from "utils/createTheme"; 10 | 11 | const Footer: React.FunctionComponent = () => { 12 | const classes = useStyles(); 13 | const { state } = React.useContext(appContext); 14 | const domainTag = state.domain.replace(".", "#"); 15 | const { main } = state.theme.palette.primary; 16 | const themeType = getThemeType(main); 17 | const handleClick = () => { 18 | window.open("https://github.com/donald-stolz", "_blank"); 19 | }; 20 | 21 | return ( 22 |
23 | 42 |
43 | 47 | 3Box Blog by Don Stolz 48 | 49 | 50 | Brooklyn Theme by Aleksey Popov 51 | 52 | 53 | Powered by Unstoppable Domains 54 | 55 |
56 |
57 | ); 58 | }; 59 | 60 | export default Footer; 61 | -------------------------------------------------------------------------------- /src/styles/pages/Write.styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, Theme, createStyles } from "@material-ui/core/styles"; 2 | 3 | export default makeStyles((theme: Theme) => 4 | createStyles({ 5 | loadingContainer: { 6 | width: "100%", 7 | minHeight: "50vh", 8 | display: "flex", 9 | alignItems: "center", 10 | justifyContent: "center", 11 | }, 12 | root: { 13 | display: "flex", 14 | flexDirection: "column", 15 | padding: theme.spacing(4), 16 | margin: theme.spacing(0, 2, 10), 17 | [theme.breakpoints.down("xs")]: { 18 | margin: theme.spacing(0, 0, 8), 19 | padding: theme.spacing(2), 20 | }, 21 | }, 22 | title: { 23 | fontSize: 32, 24 | fontWeight: "bold", 25 | }, 26 | buttonRow: { 27 | display: "flex", 28 | alignItems: "center", 29 | justifyContent: "space-between", 30 | marginTop: theme.spacing(4), 31 | [theme.breakpoints.down("xs")]: { 32 | alignItems: "flex-start", 33 | flexDirection: "column", 34 | width: "100%", 35 | }, 36 | }, 37 | draftPublishGroup: { 38 | display: "flex", 39 | alignItems: "center", 40 | [theme.breakpoints.down("xs")]: { 41 | alignItems: "flex-start", 42 | flexDirection: "column", 43 | width: "100%", 44 | }, 45 | }, 46 | label: { 47 | fontFamily: "OpenSans", 48 | fontWeight: "bold", 49 | fontSize: 16, 50 | margin: theme.spacing(3, 0, 1), 51 | }, 52 | textField: { 53 | borderRadius: 0, 54 | width: "100%", 55 | }, 56 | publishButton: { 57 | fontFamily: "OpenSans", 58 | height: 56, 59 | fontWeight: "bold", 60 | fontSize: 16, 61 | borderRadius: 28, 62 | [theme.breakpoints.down("xs")]: { 63 | width: "100%", 64 | }, 65 | }, 66 | saveButton: { 67 | fontFamily: "OpenSans", 68 | fontWeight: 600, 69 | height: 56, 70 | fontSize: 16, 71 | borderRadius: 28, 72 | marginLeft: "8px !important", 73 | [theme.breakpoints.down("xs")]: { 74 | margin: `${theme.spacing(2, 0)} !important`, 75 | width: "100%", 76 | }, 77 | }, 78 | destroyButton: { 79 | height: 56, 80 | fontFamily: "OpenSans", 81 | fontWeight: 600, 82 | color: theme.palette.error.main, 83 | paddingRight: theme.spacing(2), 84 | paddingLeft: theme.spacing(2), 85 | [theme.breakpoints.down("xs")]: { 86 | width: "100%", 87 | }, 88 | }, 89 | }) 90 | ); 91 | -------------------------------------------------------------------------------- /src/components/PostPagination.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import appContext from "services/appContext"; 3 | import useStyles from "styles/components/PostPagination.styles"; 4 | import CustomIcon from "./CustomIcon"; 5 | import Paper from "@material-ui/core/Paper"; 6 | import Button from "@material-ui/core/Button"; 7 | import useAsyncEffect from "use-async-effect"; 8 | import { getPosts } from "services/blogActions"; 9 | import { RoutingProps } from "types/app"; 10 | 11 | interface Props { 12 | postId: string; 13 | setLoading: (loading: boolean) => void; 14 | } 15 | 16 | const PostPagination: React.FunctionComponent = ({ 17 | postId, 18 | handleRoute, 19 | setLoading, 20 | }) => { 21 | const classes = useStyles(); 22 | const [neighborPosts, setNeighborPosts] = React.useState<{ 23 | next: string; 24 | previous: string; 25 | }>({ next: "", previous: "" }); 26 | const { state, dispatch } = React.useContext(appContext); 27 | const { 28 | theme: { palette }, 29 | } = state; 30 | 31 | useAsyncEffect(async () => { 32 | const posts = await getPosts({ state, dispatch })(); 33 | const index = posts.findIndex((post) => post.threadData!.postId === postId); 34 | 35 | if (index > -1) { 36 | const next = posts[index + 1] ? posts[index + 1]!.threadData!.postId : ""; 37 | const previous = posts[index - 1] 38 | ? posts[index - 1]!.threadData!.postId 39 | : ""; 40 | setNeighborPosts({ 41 | next, 42 | previous, 43 | }); 44 | } 45 | }, [postId]); 46 | 47 | const handlePrevious = () => { 48 | setLoading(true); 49 | handleRoute("read", neighborPosts.previous); 50 | }; 51 | const handleNext = () => { 52 | setLoading(true); 53 | handleRoute("read", neighborPosts.next); 54 | }; 55 | 56 | return ( 57 | 58 | 74 | 90 | 91 | ); 92 | }; 93 | 94 | export default PostPagination; 95 | -------------------------------------------------------------------------------- /src/styles/components/Header.styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, Theme, createStyles } from "@material-ui/core/styles"; 2 | 3 | export default makeStyles((theme: Theme) => 4 | createStyles({ 5 | root: { 6 | minHeight: 72, 7 | }, 8 | toolBar: { 9 | maxWidth: 1000, 10 | width: "100%", 11 | marginLeft: "auto", 12 | marginRight: "auto", 13 | minHeight: "72px !important", 14 | }, 15 | title: { 16 | fontFamily: "Merriweather !important", 17 | fontSize: "36px !important", 18 | fontWeight: "900 !important" as any, 19 | lineHeight: "1.2 !important", 20 | paddingBottom: 2, 21 | marginRight: `${theme.spacing(2)}px !important`, 22 | "&:hover": { 23 | cursor: "pointer !important", 24 | }, 25 | }, 26 | logo: { 27 | maxHeight: 68, 28 | height: 68, 29 | width: "auto", 30 | marginRight: theme.spacing(2), 31 | "&:hover": { 32 | cursor: "pointer !important", 33 | }, 34 | }, 35 | // Standard Header 36 | avatarButton: { 37 | borderRadius: "100%", 38 | padding: theme.spacing(0.25), 39 | height: 56, 40 | width: 56, 41 | }, 42 | leftContainer: { 43 | display: "flex", 44 | flexGrow: 1, 45 | alignItems: "center", 46 | }, 47 | rightContainer: { 48 | display: "flex", 49 | alignItems: "center", 50 | justifyContent: "flex-end", 51 | }, 52 | socialRow: { 53 | marginRight: theme.spacing(1), 54 | }, 55 | socialIcon: { 56 | padding: `${theme.spacing(1)}px !important`, 57 | }, 58 | headerButton: { 59 | width: 136, 60 | }, 61 | bookmarksDivider: { 62 | backgroundColor: `${theme.palette.primary.contrastText} !important`, 63 | margin: `${theme.spacing(1)}px !important`, 64 | }, 65 | bookmarksButton: { 66 | color: `${theme.palette.primary.contrastText} !important`, 67 | fontFamily: "OpenSans !important", 68 | fontWeight: "bold !important" as any, 69 | fontSize: "16px !important", 70 | marginLeft: "8px !important", 71 | }, 72 | menuItem: { 73 | fontWeight: 600, 74 | fontFamily: "OpenSans", 75 | }, 76 | menuIcon: { 77 | marginTop: theme.spacing(0.5), 78 | marginRight: theme.spacing(1), 79 | }, 80 | menuDivider: { 81 | margin: `0px ${theme.spacing(1)}px !important`, 82 | }, 83 | // MobileHeader 84 | mobileRow: { 85 | display: "flex", 86 | alignItems: "center", 87 | justifyContent: "space-between", 88 | width: "100%", 89 | }, 90 | menuButton: {}, 91 | drawer: {}, 92 | mobileButton: {}, 93 | socialHeader: { 94 | ...theme.typography.body1, 95 | fontWeight: "bold", 96 | marginTop: theme.spacing(1), 97 | marginBottom: theme.spacing(-1), 98 | }, 99 | }) 100 | ); 101 | -------------------------------------------------------------------------------- /src/components/BookmarkShare.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useStyles from "styles/components/BookmarkShare.styles"; 3 | import { 4 | addBookmark, 5 | removeBookmark, 6 | checkBookmarked, 7 | } from "services/userActions"; 8 | import appContext from "services/appContext"; 9 | import useAsyncEffect from "use-async-effect"; 10 | import IconButton from "@material-ui/core/IconButton"; 11 | import ShareIcon from "@material-ui/icons/ShareOutlined"; 12 | import DeleteIcon from "@material-ui/icons/DeleteForeverOutlined"; 13 | import BookmarkAdd from "components/BookmarkAdd"; 14 | import Bookmarked from "@material-ui/icons/Bookmark"; 15 | import SharePopup from "./SharePopup"; 16 | 17 | interface Props { 18 | postId: string; 19 | handleDelete?: () => void; 20 | } 21 | 22 | const BookmarkShare: React.FunctionComponent = ({ 23 | postId, 24 | handleDelete, 25 | }) => { 26 | const classes = useStyles(); 27 | const [isBookmarked, setIsBookmarked] = React.useState(false); 28 | const [open, setOpen] = React.useState(false); 29 | const anchorRef = React.useRef(null); 30 | const { state, dispatch } = React.useContext(appContext); 31 | const { 32 | user: { loggedIn }, 33 | } = state; 34 | 35 | useAsyncEffect(async () => { 36 | setIsBookmarked(await checkBookmarked({ state, dispatch })(postId)); 37 | }, []); 38 | 39 | const handleBookmark = async () => { 40 | if (isBookmarked) { 41 | await removeBookmark({ state, dispatch })(postId); 42 | setIsBookmarked(false); 43 | } else { 44 | await addBookmark({ state, dispatch })(postId); 45 | setIsBookmarked(true); 46 | } 47 | }; 48 | 49 | const handleOpen = () => { 50 | setOpen(true); 51 | }; 52 | 53 | const handleClose = () => { 54 | setOpen(false); 55 | }; 56 | 57 | return ( 58 |
59 | {loggedIn && ( 60 | 61 | {isBookmarked ? ( 62 | 63 | ) : ( 64 | 65 | )} 66 | 67 | )} 68 | 75 | 76 | 77 | 78 | {typeof handleDelete !== "undefined" && ( 79 | 86 | 87 | 88 | )} 89 |
90 | ); 91 | }; 92 | 93 | export default BookmarkShare; 94 | -------------------------------------------------------------------------------- /src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import appContext from "services/appContext"; 3 | import { logout } from "services/userActions"; 4 | import AppBar from "@material-ui/core/AppBar"; 5 | import Toolbar from "@material-ui/core/Toolbar"; 6 | import useStyles from "styles/components/Header.styles"; 7 | import { RoutingProps, AuthProps } from "types/app"; 8 | import Hidden from "@material-ui/core/Hidden"; 9 | import StandardHeader from "./StandardHeader"; 10 | import MobileHeader from "./MobileHeader"; 11 | 12 | const Header: React.FunctionComponent = ({ 13 | handleRoute, 14 | handleLogin, 15 | }) => { 16 | const classes = useStyles(); 17 | const { state, dispatch } = React.useContext(appContext); 18 | const { title, logo } = state; 19 | const { 20 | user: { loggedIn, walletAddress, profileImg, isAdmin, loading }, 21 | theme: { palette }, 22 | socials, 23 | } = state; 24 | 25 | const toHome = () => { 26 | handleRoute(""); 27 | }; 28 | 29 | const handleSocials = (id: string) => { 30 | const social = (socials as any)[id]; 31 | const url = /^(http|https|ftp):/.test(social) ? social : `//${social}`; 32 | window.open(url, "_blank"); 33 | }; 34 | 35 | const handleBookmarks = () => { 36 | handleRoute("bookmarks"); 37 | }; 38 | 39 | const handleLogout = async () => { 40 | try { 41 | await logout({ state, dispatch })(); 42 | } catch (error) { 43 | console.error(error); 44 | } 45 | }; 46 | 47 | const handleAddPost = () => { 48 | handleRoute("write"); 49 | }; 50 | 51 | return ( 52 | <> 53 | 54 | 55 | 56 | 73 | 74 | 75 | 92 | 93 | 94 | 95 | 96 | ); 97 | }; 98 | 99 | export default Header; 100 | -------------------------------------------------------------------------------- /src/images/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 / logos / twitter 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/images/three-box-logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/images/three-box-logo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/LikeShare.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useStyles from "styles/components/LikeShare.styles"; 3 | import appContext from "services/appContext"; 4 | import useAsyncEffect from "use-async-effect"; 5 | import Button from "@material-ui/core/Button"; 6 | import { 7 | getLikes, 8 | checkLiked, 9 | addLike, 10 | removeLike, 11 | } from "services/blogActions"; 12 | import Typography from "@material-ui/core/Typography"; 13 | import CustomIcon from "./CustomIcon"; 14 | import { 15 | handleFacebook, 16 | handleTwitter, 17 | handleLinkedIn, 18 | } from "utils/socialShare"; 19 | 20 | interface Props { 21 | postId: string; 22 | } 23 | 24 | const LikeShare: React.FunctionComponent = ({ postId }) => { 25 | const classes = useStyles(); 26 | const [likes, setLikes] = React.useState(0); 27 | const [isLiked, setIsLiked] = React.useState(false); 28 | const { state, dispatch } = React.useContext(appContext); 29 | const { 30 | user: { loggedIn }, 31 | } = state; 32 | const secondaryColor = state.theme.palette.secondary.main; 33 | const { contrastText } = state.theme.palette.secondary; 34 | 35 | useAsyncEffect(async () => { 36 | setLikes(await getLikes({ state, dispatch })(postId)); 37 | if (loggedIn) { 38 | const checkIsLiked = await checkLiked({ state, dispatch })(postId); 39 | setIsLiked(checkIsLiked); 40 | } 41 | }, [loggedIn]); 42 | 43 | const handleLike = async () => { 44 | if (isLiked) { 45 | await removeLike({ state, dispatch })(postId); 46 | setLikes((l) => l - 1); 47 | setIsLiked(false); 48 | } else { 49 | await addLike({ state, dispatch })(postId); 50 | setLikes((l) => l + 1); 51 | setIsLiked(true); 52 | } 53 | }; 54 | 55 | return ( 56 |
57 |
58 | {/* TIP Button -> Separate component */} 59 | {likes > -1 && ( 60 | 75 | )} 76 |
77 |
78 | 79 | Share this post on 80 | 81 | 90 | 99 | 108 |
109 |
110 | ); 111 | }; 112 | 113 | export default LikeShare; 114 | -------------------------------------------------------------------------------- /src/components/PostPreview.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Typography from "@material-ui/core/Typography"; 3 | import { BlogPost, ThreadObject, RoutingProps } from "types/app"; 4 | import Button from "@material-ui/core/Button"; 5 | import timeConverter from "utils/timeConverter"; 6 | import useStyles from "styles/components/PostPreview.styles"; 7 | import Paper from "@material-ui/core/Paper"; 8 | import BookmarkShare from "components/BookmarkShare"; 9 | import CustomIcon from "components/CustomIcon"; 10 | import appContext from "services/appContext"; 11 | 12 | interface Props extends RoutingProps { 13 | post: BlogPost; 14 | draft?: boolean; 15 | handleRemove?: (draftId: string) => void; 16 | } 17 | 18 | const PostPreview: React.FunctionComponent = ({ 19 | post, 20 | draft = false, 21 | handleRemove, 22 | handleRoute, 23 | }) => { 24 | const classes = useStyles(); 25 | const { title, description, threadData } = post; 26 | const { timestamp, postId } = threadData as ThreadObject; 27 | const date = timeConverter(timestamp); 28 | const { state } = React.useContext(appContext); 29 | const { 30 | theme: { palette }, 31 | } = state; 32 | 33 | const handleRead = () => { 34 | handleRoute("read", postId); 35 | }; 36 | 37 | const handleEdit = () => { 38 | handleRoute("write", postId); 39 | }; 40 | 41 | const onRemoveDraft = () => { 42 | if (handleRemove) { 43 | handleRemove(post.threadData?.postId as string); 44 | } 45 | }; 46 | // TODO author to human readable or wallet 47 | 48 | return ( 49 | 50 |
51 | 52 | {title} 53 | 54 | 55 |
56 |
57 | 62 | {date} • By {threadData?.author} 63 | 64 |
65 | 66 | {description} 67 | 68 |
69 | {draft ? ( 70 | <> 71 | 85 | 95 | 96 | ) : ( 97 | 111 | )} 112 |
113 |
114 | ); 115 | }; 116 | 117 | export default PostPreview; 118 | -------------------------------------------------------------------------------- /src/types/app.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "@material-ui/core/styles/createMuiTheme"; 2 | import { defaultTheme } from "utils/createTheme"; 3 | import { Dispatch } from "react"; 4 | import { AppAction } from "./actions"; 5 | 6 | declare global { 7 | interface Window { 8 | ethereum?: any; 9 | walletConnect?: any; 10 | web3?: any; 11 | } 12 | } 13 | 14 | export type AppPages = "read" | "write" | "drafts" | "bookmarks" | ""; 15 | export interface RoutingProps { 16 | handleRoute: (page: any, docId?: string) => void; 17 | } 18 | 19 | export interface AuthProps { 20 | handleLogin: () => void; 21 | // handleLogin: () => Promise; 22 | } 23 | // Interfaces 24 | export interface ConfigFile { 25 | title: string; 26 | logo?: string; 27 | theme: { 28 | primary: string; 29 | secondary: string; 30 | background: string; 31 | }; 32 | domain: string; 33 | threadAddress: string; 34 | adminWallet: string; 35 | spaceName: string; 36 | socials: TemplateSocials; 37 | } 38 | 39 | export interface AppContext { 40 | state: AppState; 41 | dispatch: Dispatch; 42 | } 43 | export interface AppState { 44 | box: any; 45 | space: any; 46 | thread: any; 47 | user: User; 48 | posts?: BlogPost[]; 49 | theme: Theme; 50 | domain: string; 51 | title: string; 52 | logo?: string; 53 | threadAddress: string; 54 | adminWallet: string; 55 | moderators: string[]; // did3:... 56 | moderatorNames?: { [key: string]: string }; // {"did3:...": "User Name"} 57 | spaceName: string; 58 | socials: TemplateSocials; 59 | } 60 | 61 | export interface TemplateSocials { 62 | hasFacebook: boolean; 63 | hasInstagram: boolean; 64 | hasLinkedIn: boolean; 65 | hasMedium: boolean; 66 | hasTelegram: boolean; 67 | hasTwitter: boolean; 68 | hasYouTube: boolean; 69 | hasWebsite: boolean; 70 | facebook: string; 71 | instagram: string; 72 | linkedIn: string; 73 | medium: string; 74 | telegram: string; 75 | twitter: string; 76 | youTube: string; 77 | website: string; 78 | iconColor: string; 79 | } 80 | 81 | export interface User { 82 | loggedIn: boolean; 83 | loading: boolean; 84 | walletAddress?: string; 85 | profileImg?: string; 86 | bookmarksSpace?: any; 87 | isAdmin?: boolean; 88 | did3?: string; 89 | } 90 | 91 | export interface BlogPost { 92 | title: string; 93 | description: string; 94 | tags: string[]; 95 | body: string; 96 | threadData?: ThreadObject; 97 | } 98 | 99 | export interface DraftPost { 100 | id: string; 101 | post: string; 102 | } 103 | 104 | export interface ThreadObject { 105 | author: string; 106 | message: string; 107 | postId: string; 108 | timestamp: number; 109 | } 110 | 111 | // default values 112 | export const initialState: AppState = { 113 | box: null, 114 | space: null, 115 | thread: null, 116 | user: { 117 | loggedIn: false, 118 | loading: false, 119 | }, 120 | posts: undefined, 121 | theme: defaultTheme, 122 | domain: "", 123 | title: "", 124 | threadAddress: "", 125 | adminWallet: "", 126 | moderators: [""], 127 | spaceName: "", 128 | socials: { 129 | hasWebsite: false, 130 | hasFacebook: false, 131 | hasInstagram: false, 132 | hasLinkedIn: false, 133 | hasMedium: false, 134 | hasTelegram: false, 135 | hasTwitter: false, 136 | hasYouTube: false, 137 | website: "", 138 | facebook: "", 139 | instagram: "", 140 | linkedIn: "", 141 | medium: "", 142 | telegram: "", 143 | twitter: "", 144 | youTube: "", 145 | iconColor: "#FFFFFF", 146 | }, 147 | }; 148 | 149 | export const FAILED_TO_LOAD: BlogPost = { 150 | title: "Failed to load", 151 | description: "Failed to load", 152 | tags: ["Failed to load"], 153 | body: "Failed to load", 154 | }; 155 | 156 | export type WALLET_TYPE = "web3" | "walletConnect"; 157 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import Context from "services/appContext"; 3 | import appReducer from "services/appReducer"; 4 | import { initApp } from "services/blogActions"; 5 | import { initialState, AppPages, WALLET_TYPE } from "types/app"; 6 | import CssBaseline from "@material-ui/core/CssBaseline"; 7 | import useStyles from "styles/App.styles"; 8 | import ThemeProvider from "@material-ui/styles/ThemeProvider"; 9 | import useAsyncEffect from "use-async-effect"; 10 | import CircularProgress from "@material-ui/core/CircularProgress"; 11 | import Header from "components/Header"; 12 | import Footer from "components/Footer"; 13 | import Home from "pages/Home"; 14 | import Write from "pages/Write"; 15 | import Read from "pages/Read"; 16 | import Bookmarks from "pages/Bookmarks"; 17 | import Drafts from "pages/Drafts"; 18 | import { Theme } from "@material-ui/core/styles/createMuiTheme"; 19 | import { defaultTheme } from "utils/createTheme"; 20 | import useQueryString from "utils/useQueryStringParams"; 21 | import WalletModal from "components/WalletModal"; 22 | import { login } from "services/userActions"; 23 | 24 | const App: React.FunctionComponent = () => { 25 | const classes = useStyles(); 26 | const [state, dispatch] = React.useReducer(appReducer, initialState); 27 | const [theme, setTheme] = React.useState(defaultTheme); 28 | const [loading, setLoading] = React.useState(true); 29 | const [modalOpen, setModalOpen] = React.useState(false); 30 | 31 | const { value: route, onSetValue: setRoute } = useQueryString("page", ""); 32 | const { value: docId, onSetValue: setDocId } = useQueryString("id", ""); 33 | 34 | useAsyncEffect(async () => { 35 | console.time("finish initApp()"); 36 | await initApp({ state, dispatch })(); 37 | console.timeLog("finish initApp()"); 38 | setLoading(false); 39 | }, []); 40 | 41 | const toggleModalOpen = () => { 42 | setModalOpen(!modalOpen); 43 | }; 44 | 45 | const handleLogin = async (wallet: WALLET_TYPE) => { 46 | try { 47 | toggleModalOpen(); 48 | await login({ state, dispatch })(wallet); 49 | } catch (error) { 50 | console.error(error); 51 | } 52 | }; 53 | 54 | const handleRoute = (page: AppPages, docId?: string) => { 55 | setRoute(page); 56 | setDocId(docId || ""); 57 | window.scroll({ top: 0 }); 58 | }; 59 | 60 | useEffect(() => { 61 | setTheme(state.theme); 62 | }, [state.theme]); 63 | 64 | return ( 65 | 66 | 67 | 68 | {loading ? ( 69 |
70 | 71 |
72 | ) : ( 73 | <> 74 |
75 |
76 | {route === "read" ? ( 77 | 78 | ) : route === "write" ? ( 79 | 84 | ) : route === "bookmarks" ? ( 85 | 86 | ) : route === "drafts" ? ( 87 | 88 | ) : ( 89 | 90 | )} 91 |
92 |