From 55bf6d16636852ba02b46038f3e2439cfbebd97b Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 19 Aug 2022 09:58:35 -0400 Subject: [PATCH] added syncing structure --- .env.example | 1 + package-lock.json | 91 ++++++++++++++++++++++ package.json | 1 + src/@types/global.d.ts | 7 ++ src/App.tsx | 2 + src/components/BlogComponent.tsx | 27 +++++++ src/components/Home.tsx | 8 +- src/components/NewBlog.tsx | 4 +- src/components/SyncWorker.tsx | 75 ++++++++++++++++++ src/slices/dbSlice.ts | 41 ++++++---- src/slices/viewState.ts | 8 +- src/styles/app.scss | 1 + src/styles/components/_blog_component.scss | 35 +++++++++ src/styles/pages/_home_layout.scss | 5 +- src/utils/api.ts | 10 ++- src/utils/config.ts | 5 +- src/utils/helper.ts | 12 +++ 17 files changed, 302 insertions(+), 31 deletions(-) create mode 100644 src/components/BlogComponent.tsx create mode 100644 src/components/SyncWorker.tsx create mode 100644 src/styles/components/_blog_component.scss diff --git a/.env.example b/.env.example index e90b778..d9bda0e 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ REACT_APP_WEB3_STORAGE_API_TOKEN= +REACT_APP_WEB3_STORAGE_API_DB_TOKEN= REACT_APP_WEB3_NAME= \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 254d9e3..4cec8d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "process": "^0.11.10", "react": "^18.2.0", "react-app-rewired": "^2.2.1", + "react-avatar": "^5.0.3", "react-dom": "^18.2.0", "react-error-overlay": "^6.0.11", "react-icons": "^4.4.0", @@ -7276,6 +7277,14 @@ "node": ">=10" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "engines": { + "node": "*" + } + }, "node_modules/check-types": { "version": "11.1.2", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz", @@ -7841,6 +7850,14 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "engines": { + "node": "*" + } + }, "node_modules/crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", @@ -12415,6 +12432,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "node_modules/is-callable": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", @@ -12661,6 +12683,11 @@ "node": ">=0.10.0" } }, + "node_modules/is-retina": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-retina/-/is-retina-1.0.3.tgz", + "integrity": "sha512-/tCmbIETZwCd8uHWO+GvbRa7jxwHFHdfetHfiwoP0aN9UDf3prUJMtKn7iBFYipYhqY1bSTjur8hC/Dakt8eyw==" + }, "node_modules/is-retry-allowed": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", @@ -15588,6 +15615,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -18820,6 +18857,21 @@ "semver": "bin/semver" } }, + "node_modules/react-avatar": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/react-avatar/-/react-avatar-5.0.3.tgz", + "integrity": "sha512-DNc+qkWH9QehSEZqHBhqpXWsPY+rU9W7kD68QFHfu8Atfsvx/3ML0DzAePgTUd96nCXQQ3KZMcC3LKYT8FiBIg==", + "dependencies": { + "is-retina": "^1.0.3", + "md5": "^2.0.0" + }, + "peerDependencies": { + "@babel/runtime": ">=7", + "core-js-pure": ">=3", + "prop-types": "^15.0.0 || ^16.0.0", + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -28711,6 +28763,11 @@ "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==" }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==" + }, "check-types": { "version": "11.1.2", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz", @@ -29171,6 +29228,11 @@ "which": "^2.0.1" } }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==" + }, "crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", @@ -32714,6 +32776,11 @@ "has-tostringtag": "^1.0.0" } }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "is-callable": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", @@ -32866,6 +32933,11 @@ "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==" }, + "is-retina": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-retina/-/is-retina-1.0.3.tgz", + "integrity": "sha512-/tCmbIETZwCd8uHWO+GvbRa7jxwHFHdfetHfiwoP0aN9UDf3prUJMtKn7iBFYipYhqY1bSTjur8hC/Dakt8eyw==" + }, "is-retry-allowed": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", @@ -35075,6 +35147,16 @@ "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==" }, + "md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "requires": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -37317,6 +37399,15 @@ } } }, + "react-avatar": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/react-avatar/-/react-avatar-5.0.3.tgz", + "integrity": "sha512-DNc+qkWH9QehSEZqHBhqpXWsPY+rU9W7kD68QFHfu8Atfsvx/3ML0DzAePgTUd96nCXQQ3KZMcC3LKYT8FiBIg==", + "requires": { + "is-retina": "^1.0.3", + "md5": "^2.0.0" + } + }, "react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", diff --git a/package.json b/package.json index f81211b..ea4bbef 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "process": "^0.11.10", "react": "^18.2.0", "react-app-rewired": "^2.2.1", + "react-avatar": "^5.0.3", "react-dom": "^18.2.0", "react-error-overlay": "^6.0.11", "react-icons": "^4.4.0", diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 9da4b47..13aa399 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -8,6 +8,8 @@ interface IBlog { Body: string; Creator: string; Followers: string[]; + CID?: string; + Date?: Date; } interface IUser { @@ -21,3 +23,8 @@ interface IDatabase { Blogs: string[]; Users: IUser[]; } + +interface IDatabaseAction { + Type: "ADD_BLOG" | "ADD_USER"; + CID: string; +} diff --git a/src/App.tsx b/src/App.tsx index 2b900b5..d207b91 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,12 +5,14 @@ import {NotificationContainer} from 'components/Notification'; import 'react-notifications/lib/notifications.css'; import Loader from 'components/Loader'; import { useSelector } from 'react-redux'; +import SyncWorker from 'components/SyncWorker'; function App() { // const loading = useSelector((store: any) => store.viewStates.loading); return (
+ {/* {loading && } */} diff --git a/src/components/BlogComponent.tsx b/src/components/BlogComponent.tsx new file mode 100644 index 0000000..dfb8164 --- /dev/null +++ b/src/components/BlogComponent.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import Avatar from 'react-avatar'; + +interface IBlogComponent { + blog: IBlog; +} + +const BlogComponent = (props: IBlogComponent) => { + const { blog } = props; + + return ( +
+
+ +
User
+
{blog.Date?.toLocaleString() || ""}
+
+

{blog.Title}

+
+
+ ); +}; + +export default BlogComponent; diff --git a/src/components/Home.tsx b/src/components/Home.tsx index bac306e..84d9c4d 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -1,11 +1,15 @@ import React from 'react'; +import { useSelector } from 'react-redux'; +import BlogComponent from './BlogComponent'; interface IHome {} const Home = (props: IHome) => { + const blogs = useSelector(state => state.db.Blogs) || []; + return ( -
- Home +
+ {blogs.map(blog => ())}
); }; diff --git a/src/components/NewBlog.tsx b/src/components/NewBlog.tsx index 73ff96f..bacba71 100644 --- a/src/components/NewBlog.tsx +++ b/src/components/NewBlog.tsx @@ -34,9 +34,9 @@ const NewBlog = (props: INewBlog) => { }; dispatch(createBlog(blog)).then(res => { - alert('res'); + console.log({res}); }, err => { - alert('err'); + console.log({err}); }); }; diff --git a/src/components/SyncWorker.tsx b/src/components/SyncWorker.tsx new file mode 100644 index 0000000..c0c3900 --- /dev/null +++ b/src/components/SyncWorker.tsx @@ -0,0 +1,75 @@ +import React, { useEffect } from "react"; +import { Service, Upload, Web3Storage } from "web3.storage"; +import config from "utils/config"; +import api from "utils/api"; +import { getStorageItem, getW3link, setStorageItem, sleep } from "utils/helper"; +import { AppDispatch } from "slices/store"; +import { useDispatch } from "react-redux"; +import { resetFromLocalStorage } from "slices/dbSlice"; + +interface ISyncWorker {} + +const SyncWorker = (props: ISyncWorker) => { + const dispatch = useDispatch(); + let blogs: IBlog[] = []; + + useEffect(() => { + sync(); + }, []); + + const sync = async () => { + const clientDb = new Web3Storage({ + token: config.REACT_APP_WEB3_STORAGE_API_DB_TOKEN, + } as Service); + + const uploadObjects: Upload[] = []; + for await (const upload of clientDb.list()) { + uploadObjects.push(upload); + } + + uploadObjects.sort((a, b) => (a.created > b.created) ? 1 : -1) + + blogs = getStorageItem(config.LAST_SYNC_BLOGS, []) || []; + const lastSyncNumber = getStorageItem(config.LAST_SYNC_NUMBER, 0) || 0; + + console.log({blogs, lastSyncNumber}); + for (let i = lastSyncNumber; i < uploadObjects.length; i++) { + const res = await clientDb.get(uploadObjects[i].cid); + if (!res) { + continue; + } + + const files = await res.files(); + for (const file of files) { + const contentRes = await api.get(getW3link(file.cid)); + await doDbAction(contentRes.data, uploadObjects[i]); + } + + setStorageItem(config.LAST_SYNC_NUMBER, i + 1); + } + + blogs = []; + dispatch(resetFromLocalStorage()); + }; + + // dispose individual action + const doDbAction = async (action: IDatabaseAction, upload: Upload) => { + console.log(action); + + switch (action.Type) { + case "ADD_BLOG": + const res = await api.get(getW3link(action.CID)); + blogs.push({ + ...res.data, + CID: action.CID, + Date: new Date(upload.created) + }); + setStorageItem(config.LAST_SYNC_BLOGS, blogs); + break; + } + } + + return null; +}; + +export default SyncWorker; diff --git a/src/slices/dbSlice.ts b/src/slices/dbSlice.ts index 128aa51..f925f2b 100644 --- a/src/slices/dbSlice.ts +++ b/src/slices/dbSlice.ts @@ -1,11 +1,11 @@ import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; -import { convertTitle } from 'utils/helper'; +import { convertTitle, getStorageItem } from 'utils/helper'; import { Service, Web3Storage } from 'web3.storage'; -import * as Name from 'w3name'; import config from 'utils/config'; const client = new Web3Storage({ token: config.REACT_APP_WEB3_STORAGE_API_TOKEN } as Service); -const name = Name.parse(config.REACT_APP_WEB3_NAME); +const clientDb = new Web3Storage({ token: config.REACT_APP_WEB3_STORAGE_API_DB_TOKEN } as Service); + const initialState = { Blogs: [] as string[], @@ -15,37 +15,46 @@ const initialState = { export const createBlog = createAsyncThunk('createBlog', async (blog: IBlog) => { try { const title = convertTitle(blog.Title); - const blob = new File([JSON.stringify(blog, null, 2)], title, {type: "application/json"}) + let blob = new File([JSON.stringify(blog, null, 2)], title + "_1", {type: "application/json"}) const rootCid = await client.put([blob], { name: title, maxRetries: 3, + wrapWithDirectory: false }); - const name = Name.parse(config.REACT_APP_WEB3_NAME); - const revision = await Name.resolve(name); - + const dbAction: IDatabaseAction = { + Type: "ADD_BLOG", + CID: rootCid + }; + + blob = new File([JSON.stringify(dbAction, null, 2)], title + "_1", {type: "application/json"}) + await clientDb.put([blob], { + name: "add_blog_" + title, + maxRetries: 3, + wrapWithDirectory: false + }); - // const nextRevision = await Name.increment(revision, nextValue); - // await Name.publish(nextRevision, name.key); + return blog; } catch { - return null; + return false; } }); - export const dbSlice = createSlice({ name: 'dbSlice', // `createSlice` will infer the state type from the `initialState` argument initialState, - reducers: {}, + reducers: { + resetFromLocalStorage: (state: IDatabase, action: PayloadAction) => { + state.Blogs = getStorageItem(config.LAST_SYNC_BLOGS, []) || []; + } + }, extraReducers: (builder) => { builder.addCase(createBlog.fulfilled, (state: any, action: PayloadAction) => { - state.web3 = action.payload?.web3; - console.log({ action }); - state.selectedAddress = action.payload?.account; - state.chainId = action.payload?.chainId; }); }, }); +export const {resetFromLocalStorage} = dbSlice.actions; + export default dbSlice.reducer; diff --git a/src/slices/viewState.ts b/src/slices/viewState.ts index 38f3d9d..eb75aae 100644 --- a/src/slices/viewState.ts +++ b/src/slices/viewState.ts @@ -5,8 +5,8 @@ const initialState = { loading: false }; -export const web3Slice = createSlice({ - name: 'web3', +export const viewState = createSlice({ + name: 'viewState', // `createSlice` will infer the state type from the `initialState` argument initialState, reducers: { @@ -18,6 +18,6 @@ export const web3Slice = createSlice({ }, }); -export const {setLoading} = web3Slice.actions; +export const {setLoading} = viewState.actions; -export default web3Slice.reducer; +export default viewState.reducer; diff --git a/src/styles/app.scss b/src/styles/app.scss index c42a76e..3f9e0b2 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -4,6 +4,7 @@ @import './components/footer'; @import './components/navbar'; @import './components/new_blog_page'; +@import './components/blog_component'; @import './pages/home_layout'; @import './pages/welcome_page'; diff --git a/src/styles/components/_blog_component.scss b/src/styles/components/_blog_component.scss new file mode 100644 index 0000000..878aa7f --- /dev/null +++ b/src/styles/components/_blog_component.scss @@ -0,0 +1,35 @@ +.blog-item { + padding: 20px; + border-bottom: 1px solid #ccc; + + .user { + display: flex; + align-items: center; + + .name { + margin: 0 0 0 6px; + font-size: 14px; + color: #666; + cursor: pointer; + } + + .date { + margin: 0 0 0 auto; + font-size: 12px; + color: #666; + cursor: pointer; + } + } + + .blog-title { + margin: 12px 0 8px 0; + line-height: 28px; + font-size: 22px; + cursor: pointer; + } + + .blog-body { + font-size: 16px; + cursor: pointer; + } +} \ No newline at end of file diff --git a/src/styles/pages/_home_layout.scss b/src/styles/pages/_home_layout.scss index 2420aec..0bc8299 100644 --- a/src/styles/pages/_home_layout.scss +++ b/src/styles/pages/_home_layout.scss @@ -19,9 +19,10 @@ border: 1px solid #ddd; border-top: none; border-bottom: none; - height: 100%; + height: 100vh; width: 70%; height: 100vh; - padding: 35px 10px 10px 10px; + padding: 20px 10px 10px 10px; + overflow-y: auto; } } diff --git a/src/utils/api.ts b/src/utils/api.ts index 02d431d..ae8ede9 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -1,8 +1,10 @@ -import axios from 'axios'; +import axios from "axios"; const api = axios.create({ params: {}, - headers: {}, + headers: { + "content-type": "application/json;charset=UTF-8", + }, }); export function sendFormData(url: string, method: string, FD: FormData) { @@ -10,7 +12,7 @@ export function sendFormData(url: string, method: string, FD: FormData) { const XHR = new XMLHttpRequest(); // Define what happens on successful data submission - XHR.addEventListener('load', function (event: any) { + XHR.addEventListener("load", function (event: any) { if (event.target.status >= 200 && event.target.status < 300) { resolve(event.target.response); } else { @@ -19,7 +21,7 @@ export function sendFormData(url: string, method: string, FD: FormData) { }); // Define what happens in case of error - XHR.addEventListener(' error', function (event: any) { + XHR.addEventListener(" error", function (event: any) { reject(event.target.response); }); diff --git a/src/utils/config.ts b/src/utils/config.ts index d703924..63d1a2d 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,7 +1,10 @@ const config = { - appID: 'ipp', + appID: 'IPP', REACT_APP_WEB3_STORAGE_API_TOKEN: process.env.REACT_APP_WEB3_STORAGE_API_TOKEN || "", + REACT_APP_WEB3_STORAGE_API_DB_TOKEN: process.env.REACT_APP_WEB3_STORAGE_API_DB_TOKEN || "", REACT_APP_WEB3_NAME: process.env.REACT_APP_WEB3_NAME || "", + LAST_SYNC_NUMBER: "IPP_LAST_SYNC_NUMBER", + LAST_SYNC_BLOGS: "IPP_LAST_SYNC_BLOGS", }; export default config; diff --git a/src/utils/helper.ts b/src/utils/helper.ts index caae8e4..7041e7b 100644 --- a/src/utils/helper.ts +++ b/src/utils/helper.ts @@ -96,3 +96,15 @@ export function convertTitle(str: string) { } return res; } + +export function getW3link(cid: string) { + // return `https://w3s.link/ipfs/${cid}`; + // return `https://${cid}.ipfs.w3s.link/`; + return `https://braindance.mypinata.cloud/ipfs/${cid}`; +} + +export function sleep(ms: number) { + return new Promise((resolve, reject) => { + setTimeout(() => resolve(true), ms); + }) +} -- 2.51.2