├── src ├── variables.env ├── setupTests.js ├── App.test.js ├── components │ ├── PageNotFound.js │ ├── Alert.js │ ├── Page.js │ ├── Navbar.js │ ├── PreviewSection.js │ ├── SignUp.js │ ├── RolesManagement.js │ ├── Editor.js │ └── Wiki.js ├── index.css ├── App.js ├── graphql │ ├── schema.js │ └── resolvers.js ├── logo.svg ├── index.js ├── styles │ ├── PreviewSection.scss │ ├── helpers.scss │ ├── Editor.scss │ └── App.scss └── serviceWorker.js ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── LogoH-wik-8.png ├── manifest.json └── index.html ├── .gitignore ├── README.md └── package.json /src/variables.env: -------------------------------------------------------------------------------- 1 | PRODUCTION="YES" -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eyss/h-wiki-front/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eyss/h-wiki-front/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eyss/h-wiki-front/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/LogoH-wik-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eyss/h-wiki-front/HEAD/public/LogoH-wik-8.png -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/PageNotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class PageNotFound extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { 7 | 8 | } 9 | } 10 | 11 | render() { 12 | return ( 13 |
14 |

404 not found

15 |
16 | ) 17 | } 18 | } 19 | 20 | export default PageNotFound; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /.cargo 14 | /target 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # H-Wiki 2 | 3 | Frontend code for H-Wiki, see [h-wiki-back](https://github.com/eyss/h-wiki-back) for DNA code. 4 | 5 | H-Wiki is a hApp (holochain application) that allows groups and communities to create their own wiki-like repositories of information. 6 | 7 | Each wiki is created with an initial administrator, that can grant `administrator` or `editor` roles to any other user that joins the hApp. 8 | 9 | Design: https://hackmd.io/HQ0wjyjjTpK4yJ9FcAx0Iw 10 | 11 | ## Running 12 | 13 | Start the holochain backend first, see https://github.com/eyss/h-wiki-back#getting-started. 14 | 15 | Run this: 16 | 17 | ``` 18 | yarn 19 | yarn start 20 | ``` 21 | 22 | You will be automatically redirected to `https://localhost:3000`. This hApp uses the Progenitor pattern, which expects an admin user. If you've used the sample conductor config in the `h-wiki-back` repo, you should already be the progenitor. 23 | 24 | When the UI starts up, you'll be asked for your username. Once you enter it, you're ready to start creating pages! 25 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | //React Router DOM 3 | import { BrowserRouter, Route, Switch } from 'react-router-dom' 4 | import { connect } from 'react-redux'; 5 | 6 | //Styles 7 | import './styles/App.scss'; 8 | //Components 9 | import Wiki from './components/Wiki'; 10 | import RolesManagement from './components/RolesManagement'; 11 | import SignUp from './components/SignUp'; 12 | import PageNotFound from './components/PageNotFound'; 13 | 14 | 15 | function App(props) { 16 | return ( 17 | 18 | 19 | {props.userId.role === 'Admin' && 20 | 21 | 22 | 23 | } 24 | 25 | {!props.userId.userName.length && 26 | 27 | 28 | 29 | } 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | } 41 | 42 | function mapDispatchToProps(dispatch) { 43 | return {}; 44 | } 45 | 46 | function mapStateToProps(state) { 47 | return { 48 | client: state.client, 49 | userId: state.userId 50 | } 51 | } 52 | 53 | export default connect(mapStateToProps, mapDispatchToProps)(App) 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "holowiki", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@babel/runtime": "^7.8.4", 7 | "@holochain/hc-web-client": "^0.5.0", 8 | "@testing-library/jest-dom": "^4.2.4", 9 | "@testing-library/react": "^9.3.2", 10 | "@testing-library/user-event": "^7.1.2", 11 | "apollo-boost": "^0.4.7", 12 | "apollo-client": "^2.6.8", 13 | "apollo-link-schema": "^1.2.4", 14 | "compress.js": "^1.1.2", 15 | "geteventlisteners": "^1.1.0", 16 | "graphql": "^14.5.8", 17 | "graphql-tools": "^4.0.6", 18 | "holochain-file-storage": "0.0.1", 19 | "markdown-it": "^10.0.0", 20 | "node-sass": "^4.13.0", 21 | "react": "^16.12.0", 22 | "react-dom": "^16.12.0", 23 | "react-icons": "^3.8.0", 24 | "react-markdown-editor-lite": "^0.5.0", 25 | "react-redux": "^7.1.3", 26 | "react-router-dom": "^5.1.2", 27 | "react-scripts": "^3.4.0", 28 | "redux": "^4.0.5" 29 | }, 30 | "scripts": { 31 | "start": "react-scripts start", 32 | "build": "react-scripts build", 33 | "test": "react-scripts test", 34 | "eject": "react-scripts eject" 35 | }, 36 | "eslintConfig": { 37 | "extends": "react-app" 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | }, 51 | "resolutions": { 52 | "react-dev-utils": "10.1.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/components/Alert.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | 3 | class Alert extends React.Component { 4 | //eslint-disable-next-line 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render() { 10 | return ( 11 | 12 | {this.props.alert && ( 13 |
14 | {this.props.confirmation && ( 15 |
16 |
17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 | )} 25 | 26 | {this.props.preloader && ( 27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 |
35 | )} 36 | 37 | {!this.props.confirmation && !this.props.preloader && ( 38 |
39 |
40 | 41 |
42 |
43 | 44 |
45 |
46 | )} 47 |
48 | )} 49 |
50 | ); 51 | } 52 | } 53 | 54 | export default Alert; 55 | -------------------------------------------------------------------------------- /src/components/Page.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import MarkdownIt from "markdown-it"; 4 | 5 | class Page extends React.Component { 6 | mdEditor = null; 7 | mdParser = null; 8 | constructor(props) { 9 | super(props); 10 | this.article = React.createRef(); 11 | 12 | this.mdParser = new MarkdownIt(); 13 | this.container = React.createRef(); 14 | } 15 | 16 | componentDidMount = () => { 17 | this.setFnLinks(); 18 | this.props.setRenderContent( 19 | this.props.data.renderedContent, 20 | this.container.current, 21 | this.props.dataPage 22 | ); 23 | }; 24 | 25 | componentDidUpdate = () => { 26 | this.setFnLinks(); 27 | this.props.setRenderContent( 28 | this.props.data.renderedContent, 29 | this.container.current, 30 | this.props.dataPage 31 | ); 32 | }; 33 | 34 | setFnLinks = (e, _this = this) => { 35 | if (!this.article.current.added) { 36 | this.article.current.added = true; 37 | this.article.current.addEventListener("click", function (e) { 38 | if (e.target.nodeName === "A" && !e.target.getAttribute("download")) { 39 | e.preventDefault(); 40 | _this.props.showPage(e); 41 | } 42 | }); 43 | } 44 | }; 45 | 46 | render() { 47 | let data = this.props.data; 48 | return ( 49 |
50 |
51 |
52 |
53 |

{data.title}

54 |
55 |
56 |
57 |
58 | ); 59 | } 60 | } 61 | 62 | export default Page; 63 | -------------------------------------------------------------------------------- /src/graphql/schema.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-boost"; 2 | 3 | export const typeDefs = gql` 4 | type Section { 5 | id: ID! 6 | type: String! 7 | content: String! 8 | rendered_content: String! 9 | } 10 | 11 | type Page { 12 | title: String! 13 | sections: [Section!]! 14 | } 15 | 16 | type Query { 17 | page(title: String!): Page! 18 | homePage: Page! 19 | allPages: [Page!]! 20 | getId: User! 21 | getPageTitle(title: String!): [String!]! 22 | getUsername(username: String!): [String!]! 23 | getUserInfo(username: String!): [User!]! 24 | 25 | } 26 | 27 | input SectionInput { 28 | type: String! 29 | content: String! 30 | rendered_content: String! 31 | timestamp: ID! 32 | 33 | } 34 | 35 | type Mutation { 36 | createPage(title: String!): Page! 37 | createPageWithSections(title: String!, sections: [SectionInput!]!): Page! 38 | addSectionToPage(title: String!, section: SectionInput!): Page! 39 | addOrderedSectionToPage( 40 | title: String! 41 | beforeSection: ID! 42 | section: SectionInput! 43 | sections: [ID!]! 44 | mode: String! 45 | ): Page! 46 | removeSection(id: ID!): Page! 47 | updateSection(id: ID!, section: SectionInput!): Section! 48 | roleUpdate(currentRole: String!, agentAddress: ID!, newRole:String!): String! 49 | } 50 | type Role { 51 | name: String! 52 | members: [User!]! 53 | } 54 | 55 | extend type Query { 56 | allUsers: [User!]! 57 | } 58 | 59 | type User { 60 | id:ID! 61 | userName: String! 62 | role: String! 63 | } 64 | 65 | extend type Mutation { 66 | createRole(name: String!): String! 67 | assignToRole(roleName: String!, agentId: ID!): String! 68 | unassignToRole(roleName: String!, agentId: ID!): String! 69 | createUser(name: String!): User! 70 | } 71 | `; 72 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | H-Wiki 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { MdLibraryAdd, MdAccountCircle, MdAssignmentInd, MdHome } from 'react-icons/md'; 4 | class Navbar extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = {}; 8 | } 9 | refreshLinks = ()=>{ 10 | if (this.props.refreshLinks) { 11 | this.props.refreshLinks(); 12 | } 13 | } 14 | 15 | render() { 16 | return ( 17 | 66 | ) 67 | } 68 | } 69 | 70 | export default Navbar; -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-boost"; 2 | import { SchemaLink } from "apollo-link-schema"; 3 | import { InMemoryCache } from "apollo-boost"; 4 | import { ApolloClient } from "apollo-boost"; 5 | import { typeDefs } from "./graphql/schema"; 6 | import { resolvers } from "./graphql/resolvers"; 7 | import { makeExecutableSchema } from "graphql-tools"; 8 | import { connect } from "@holochain/hc-web-client"; 9 | import React from "react"; 10 | import ReactDOM from "react-dom"; 11 | import "./index.css"; 12 | import { Provider } from "react-redux"; 13 | import { createStore } from "redux"; 14 | import App from "./App"; 15 | import * as serviceWorker from "./serviceWorker"; 16 | 17 | async function start() { 18 | let parameters = { 19 | client: null, 20 | userId: null, 21 | }; 22 | 23 | const store = createStore((state = parameters, action) => { 24 | switch (action.type) { 25 | case "SET_CLIENT": 26 | return { ...state, client: action.value }; 27 | case "SET_USERID": 28 | return { ...state, userId: action.value }; 29 | case "SET_CALLZOME": 30 | return { ...state, callZome: action.value }; 31 | default: 32 | return state; 33 | } 34 | }); 35 | 36 | var client, 37 | hcConnect = connect( 38 | process.env.NODE_ENV === "development" 39 | ? { url: "ws://0.0.0.0:3400" } 40 | : undefined 41 | ), 42 | { callZome } = await hcConnect; 43 | 44 | store.dispatch({ 45 | type: "SET_CALLZOME", 46 | value: callZome, 47 | }); 48 | 49 | store.dispatch({ 50 | type: "SET_CALLZOME", 51 | value: callZome, 52 | }); 53 | 54 | await hcConnect.then((context) => { 55 | const schema = makeExecutableSchema({ 56 | typeDefs, 57 | resolvers, 58 | }); 59 | client = new ApolloClient({ 60 | cache: new InMemoryCache(), 61 | link: new SchemaLink({ schema, context }), 62 | }); 63 | 64 | store.dispatch({ 65 | type: "SET_CLIENT", 66 | value: client, 67 | }); 68 | }); 69 | 70 | await client 71 | .query({ 72 | query: gql` 73 | { 74 | getId { 75 | userName 76 | role 77 | } 78 | } 79 | `, 80 | }) 81 | .then((res) => { 82 | res = res.data.getId; 83 | let userId = { 84 | userName: "", 85 | role: "Reader", 86 | }; 87 | // If user is recived 88 | if (res.userName.length > 0) { 89 | userId = res; 90 | } 91 | 92 | store.dispatch({ 93 | type: "SET_USERID", 94 | value: userId, 95 | }); 96 | }); 97 | 98 | ReactDOM.render( 99 | 100 | 101 | , 102 | document.getElementById("root") 103 | ); 104 | } 105 | 106 | start(); 107 | // If you want your app to work offline and load faster, you can change 108 | // unregister() to register() below. Note this comes with some pitfalls. 109 | // Learn more about service workers: https://bit.ly/CRA-PWA 110 | serviceWorker.unregister(); 111 | -------------------------------------------------------------------------------- /src/components/PreviewSection.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {MdMoreVert, MdCreate, MdRemove, MdPlaylistAdd} from "react-icons/md"; 3 | import MarkdownIt from 'markdown-it'; 4 | export default class PreviewSection extends React.Component { 5 | // eslint-disable-next-line no-useless-constructor 6 | constructor(props) { 7 | super(props); 8 | this.mdParser = new MarkdownIt(); 9 | 10 | this.container = React.createRef(); 11 | } 12 | 13 | componentDidMount() { 14 | this.container.current 15 | .addEventListener('click', (e)=>{ 16 | if (e.target.nodeName === 'A') { 17 | e.preventDefault(); 18 | } 19 | }); 20 | this.props.setRenderContent(this.props.content, this.container.current); 21 | } 22 | 23 | componentDidUpdate() { 24 | this.props.setRenderContent(this.props.content, this.container.current); 25 | } 26 | 27 | showEditor = (mode) => { 28 | this.props.showEditor(mode, this.props.pos) 29 | } 30 | 31 | removeSection = () => { 32 | this.props.showConfirmation({ 33 | process: 'remove', 34 | pos: this.props.pos 35 | }); 36 | } 37 | 38 | render() { 39 | return( 40 |
41 |
42 | 43 |
44 |
45 |
    46 |
  • 47 | 50 |
      51 |
    • 52 | 55 |
    • 56 | 57 |
    • 58 | 61 |
    • 62 | 63 | 64 | {this.props.pos === 0 && 65 |
    • 66 | 69 |
    • 70 | } 71 | 72 |
    • 73 | 76 |
    • 77 | 78 |
    79 |
  • 80 |
81 |
82 |
83 | 84 |
85 | ) 86 | } 87 | } -------------------------------------------------------------------------------- /src/components/SignUp.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Navbar from './Navbar'; 3 | import Alert from './Alert'; 4 | import {MdPerson} from 'react-icons/md'; 5 | import { connect } from 'react-redux'; 6 | /** Apollo cliente, GraphQL */ 7 | import { gql } from "apollo-boost"; 8 | import {Redirect} from 'react-router-dom'; 9 | 10 | class SignUp extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | username: '', 15 | alert: false, 16 | confirmation: false, 17 | confirmationMsg: '', 18 | preloader: false, 19 | preloaderMsg: '', 20 | loadingPage: true, 21 | valmsg: '' 22 | 23 | } 24 | this.username = React.createRef(); 25 | } 26 | 27 | componentDidMount() { 28 | this.setState({loadingPage: false}); 29 | } 30 | 31 | setUsername = (e) => { 32 | if (!/\s/.test(e.target.value)) { 33 | this.setState({ 34 | username: e.target.value 35 | }); 36 | } 37 | } 38 | 39 | registerUser(e) { 40 | e.preventDefault(); 41 | let unl = this.state.username.length; 42 | if (unl < 3 ) { 43 | let valmsg = unl === 0 ? 44 | 'The username field is required' : 45 | 'The username must be at least 3 characters'; 46 | this.setState({ valmsg }); 47 | } else { 48 | this.setState({ 49 | preloader: true, 50 | alert: true, 51 | preloaderMsg: 'registering user' 52 | }); 53 | 54 | this.props.client 55 | .mutate({ 56 | mutation: gql` 57 | mutation createUser($name: String!) { 58 | createUser(name: $name) { 59 | userName 60 | role 61 | } 62 | } 63 | `, 64 | variables: { 65 | name: this.state.username 66 | } 67 | }).then(res => { 68 | this.props.setUserId(res.data.createUser); 69 | }).catch(err => { 70 | console.log(err); 71 | }).finally(e => { 72 | return ( 73 | 74 | ); 75 | }); 76 | } 77 | } 78 | 79 | render() { 80 | return ( 81 |
82 | 88 |
89 |
90 |
91 |
92 | 93 |
94 |
95 | 96 |
97 |
98 |
this.registerUser(e)}> 99 |
100 | this.setUsername(e) } 103 | value={this.state.username} 104 | ref={this.username} 105 | autoFocus 106 | /> 107 |
108 | 109 |
110 |
111 |
112 | 113 |
114 |
115 |
116 | {this.state.loadingPage &&
} 117 |
118 | 119 | 128 |
129 | ) 130 | } 131 | } 132 | 133 | function mapDispatchToProps(dispatch) { 134 | return { 135 | setUserId: (userId) =>{ 136 | dispatch({ 137 | type: 'SET_USERID', 138 | value: userId 139 | }); 140 | } 141 | }; 142 | } 143 | 144 | function mapStateToProps(state) { 145 | return { 146 | client: state.client, 147 | userId: state.userId 148 | } 149 | } 150 | 151 | export default connect(mapStateToProps, mapDispatchToProps)(SignUp) -------------------------------------------------------------------------------- /src/styles/PreviewSection.scss: -------------------------------------------------------------------------------- 1 | div.preview-section{ 2 | display: flex; 3 | margin-bottom: 1em; 4 | padding-top: 1em; 5 | min-height: 75px; 6 | 7 | >div:first-child{ 8 | width: 100%; 9 | box-shadow: oPrimaryColor(.8) -2px 0px 0px inset; 10 | background-color: #f9f9f9; 11 | 12 | border-radius: .4em; 13 | padding: 1em; 14 | box-sizing: border-box; 15 | font-size: 14px; 16 | color: #707070; 17 | border: 1px solid rgba(243,243,243, .6); 18 | border-right: none; 19 | img, video{ 20 | width: 100%!important; 21 | } 22 | } 23 | 24 | // Contenedor del boton para elminar la seccion 25 | > div:last-child{ 26 | width: 60px; 27 | min-width: 60px; 28 | display: flex; 29 | justify-content: center; 30 | 31 | >div{ 32 | position: relative; 33 | 34 | > ul { 35 | position: relative; 36 | top: .2em; 37 | > li { 38 | list-style: none; 39 | position: relative; 40 | 41 | > button { 42 | justify-content: center; 43 | background-color: transparent; 44 | border: none; 45 | font-size: 1.5em; 46 | cursor: pointer; 47 | color: $primary-color; 48 | outline: 0; 49 | padding: .1em 0; 50 | 51 | transition-duration: .2s; 52 | border-radius: .3em; 53 | z-index: 0; 54 | 55 | &:hover{ 56 | background-color: oPrimaryColor(.2); 57 | } 58 | } 59 | 60 | &:hover > ul { display: flex; } 61 | 62 | > ul { 63 | z-index: 1; 64 | background-color: #EBECF1; 65 | box-shadow: 0 0 3px 0 rgba(0,0,0,.08); 66 | border-radius: .5em; 67 | display: none; 68 | flex-direction: column; 69 | right: 0px; 70 | width: 150px; 71 | position: absolute; 72 | 73 | > li { 74 | width: 100%; 75 | list-style: none; 76 | 77 | >button { 78 | outline: 0; 79 | font-size: .8em; 80 | border: none; 81 | padding: 1em .2em; 82 | cursor: pointer; 83 | background-color: transparent; 84 | transition-duration: .3s; 85 | color: #636363; 86 | 87 | &:first-child{ 88 | border-top-left-radius: .5em; 89 | border-top-right-radius: .5em; 90 | } 91 | &:last-child{ 92 | border-bottom-left-radius: .5em; 93 | border-bottom-right-radius: .5em; 94 | } 95 | 96 | &:hover { 97 | background-color: oPrimaryColor(.1); 98 | } 99 | 100 | > svg { 101 | font-size: 1.6em; 102 | // color: $primary-color; 103 | padding: 0 .25em; 104 | } 105 | } 106 | } 107 | } 108 | 109 | button { 110 | width: 100%; 111 | display: flex; 112 | align-items: center; 113 | } 114 | } 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' } 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready.then(registration => { 134 | registration.unregister(); 135 | }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/styles/helpers.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #5db68e; 2 | $bg1 : #F0F1F4; 3 | $bg2 : #FAFAFA; 4 | 5 | @function oPrimaryColor($opacity) { 6 | @return rgba(93,182,142, $opacity); 7 | } 8 | 9 | *{ margin: 0; padding: 0; } 10 | 11 | textarea::-webkit-scrollbar, 12 | div::-webkit-scrollbar{ width: 8px; height: 8px; } 13 | textarea::-webkit-scrollbar-track, 14 | div::-webkit-scrollbar-track{ background: #f5f5f5; border-radius: 2em; } 15 | textarea::-webkit-scrollbar-thumb, 16 | div::-webkit-scrollbar-thumb{ background: #cccccc; border-radius: 2em; } 17 | 18 | input.readonly{ pointer-events: none; } 19 | 20 | @mixin modalPropertys($zindex) { 21 | background-color: oPrimaryColor(0.6); 22 | width: 100vw; 23 | height: 100vh; 24 | position: absolute; 25 | top: 0px; 26 | left: 0px; 27 | display: flex; 28 | justify-content: center; 29 | align-items: center; 30 | z-index: $zindex; 31 | } 32 | 33 | @mixin buttonPropertys($style, $width : null, $height: null) { 34 | border: none; 35 | padding: .3em 0; 36 | border-radius: .3em; 37 | outline: 0; 38 | cursor: pointer; 39 | 40 | @if ($width != null){ width: $width; } @else { width: auto; }; 41 | @if ($height != null){ height: $height; } @else { height: auto; }; 42 | 43 | @if $style == 'normal' { 44 | background-color: $primary-color; 45 | color: #FFFFFF; 46 | } @else if $style == 'contoured' { 47 | border: $primary-color 1px solid; 48 | background-color: transparent; 49 | color: $primary-color; 50 | } 51 | } 52 | 53 | // Preloader animation 54 | div.linear-preloader { 55 | display: block; 56 | top:100%; 57 | width: 100%; 58 | background-color: oPrimaryColor(.3); 59 | border-radius: 2px; 60 | min-height: 3px; 61 | 62 | >div { 63 | background-color: $primary-color; 64 | 65 | &:before { 66 | content: ''; 67 | position: absolute; 68 | background-color: inherit; 69 | top: 0; 70 | left: 0; 71 | bottom: 0; 72 | will-change: left, right; 73 | -webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite; 74 | animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite; 75 | } 76 | 77 | &:after { 78 | content: ''; 79 | position: absolute; 80 | background-color: inherit; 81 | top: 0; 82 | left: 0; 83 | bottom: 0; 84 | will-change: left, right; 85 | -webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite; 86 | animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite; 87 | -webkit-animation-delay: 1.15s; 88 | animation-delay: 1.15s 89 | } 90 | } 91 | } 92 | 93 | div.simple-preloader{ 94 | display: block; 95 | position: relative; 96 | width: 100%; 97 | background-color: white; 98 | min-height: 5px; 99 | 100 | &::before{ 101 | content: ''; 102 | position: absolute; 103 | height: 3px; 104 | background-color: oPrimaryColor(.3); 105 | top: 50%; 106 | transform: translateY(-50%); 107 | width: 100%; 108 | } 109 | 110 | &::after{ 111 | content: ''; 112 | position: absolute; 113 | height: 3px; 114 | background-color: $primary-color; 115 | top: 50%; 116 | transform: translateY(-50%); 117 | -webkit-animation: simple-preloader 1.3s infinite; 118 | animation: simple-preloader 1.3s infinite; 119 | } 120 | } 121 | 122 | @-webkit-keyframes indeterminate { 123 | 0% { 124 | left: -35%; 125 | right: 100% 126 | } 127 | 60% { 128 | left: 100%; 129 | right: -90% 130 | } 131 | 100% { 132 | left: 100%; 133 | right: -90% 134 | } 135 | } 136 | @keyframes indeterminate { 137 | 0% { 138 | left: -35%; 139 | right: 100% 140 | } 141 | 60% { 142 | left: 100%; 143 | right: -90% 144 | } 145 | 100% { 146 | left: 100%; 147 | right: -90% 148 | } 149 | } 150 | 151 | @-webkit-keyframes indeterminate-short { 152 | 0% { 153 | left: -200%; 154 | right: 100% 155 | } 156 | 60% { 157 | left: 107%; 158 | right: -8% 159 | } 160 | 100% { 161 | left: 107%; 162 | right: -8% 163 | } 164 | } 165 | @keyframes indeterminate-short { 166 | 0% { 167 | left: -200%; 168 | right: 100% 169 | } 170 | 60% { 171 | left: 107%; 172 | right: -8% 173 | } 174 | 100% { 175 | left: 107%; 176 | right: -8% 177 | } 178 | } 179 | 180 | @-webkit-keyframes simple-preloader { 181 | 0% { 182 | left: 0; 183 | width: 0%; 184 | } 185 | 45% { 186 | left: 0; 187 | width: 100%; 188 | } 189 | 100% { 190 | left: 100%; 191 | width: 0% 192 | } 193 | } 194 | @keyframes simple-preloader { 195 | 0% { 196 | left: 0; 197 | width: 0%; 198 | } 199 | 45% { 200 | left: 0; 201 | width: 100%; 202 | } 203 | 100% { 204 | left: 100%; 205 | width: 0% 206 | } 207 | } 208 | 209 | span[title='Underline']{ display: none!important; } 210 | 211 | div#alert{ 212 | @include modalPropertys(30000); 213 | >div{ 214 | display: flex; 215 | flex-direction: column; 216 | background-color: #FFFFFF; 217 | padding: .5em 1em; 218 | border-radius: .4em; 219 | min-width: 306px; 220 | max-width: 306px; 221 | } 222 | 223 | >div.confirmation{ 224 | 225 | position: relative; 226 | top: -15%; 227 | >div:first-child{ 228 | padding: 1.7em 0; 229 | >label{ 230 | color: #6f6f6f; 231 | font-size: 0.983em; 232 | } 233 | } 234 | 235 | >div:last-child{ 236 | display: flex; 237 | justify-content: flex-end; 238 | >button { 239 | &:first-child{ 240 | @include buttonPropertys('contoured', 40px, 28px); 241 | transition-duration: .3s; 242 | margin-right: 1em; 243 | &:hover { background-color: oPrimaryColor(.1); } 244 | } 245 | &:last-child{ 246 | @include buttonPropertys('normal', 40px, 28px); 247 | } 248 | } 249 | } 250 | } 251 | 252 | >div.preloader{ 253 | position: relative; 254 | >div:first-child{ 255 | display: flex; 256 | justify-content: center; 257 | padding: .8em 0; 258 | >label { 259 | color: #6f6f6f; 260 | font-size: 0.975em; 261 | } 262 | } 263 | 264 | >div:last-child{ 265 | position: relative; 266 | padding: .2em 0; 267 | } 268 | } 269 | 270 | >div.alert{ 271 | background-color: #f3f3f3; 272 | border: 1px solid rgba(97,97,97, .1); 273 | >div{ 274 | display: flex; 275 | justify-content: center; 276 | } 277 | >div:first-child{ 278 | padding: .5em 0; 279 | >label{ 280 | font-size: .87em; 281 | color: #888888; 282 | } 283 | } 284 | 285 | >div:last-child{ 286 | padding-top: .5em; 287 | >button{ 288 | @include buttonPropertys('normal', 80px); 289 | } 290 | } 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/components/RolesManagement.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { gql } from "apollo-boost"; 3 | import Navbar from './Navbar'; 4 | import Alert from './Alert'; 5 | import { MdAssignmentInd, MdSync, MdSearch, MdAccountBox } from 'react-icons/md'; 6 | import { connect } from 'react-redux'; 7 | 8 | class RolesManagement extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | userName: '', 13 | role: '', 14 | currentRole: '', 15 | agentAddress: '', 16 | users: [], 17 | userSelected: false, 18 | valmsg: '', 19 | waitingMessage: '', 20 | 21 | alert: false, 22 | preloader: false, 23 | preloaderMsg: '', 24 | } 25 | } 26 | 27 | setRole = (e)=>{ 28 | this.setState({ 29 | role: e.target.value, 30 | valmsg: '' 31 | }); 32 | } 33 | 34 | setUsername = (e)=> { 35 | this.setState({ 36 | userName: e.target.value 37 | }, (_this = this)=>{ 38 | let username = _this.state.userName; 39 | if (username.length >= 3) { 40 | _this.setState({ waitingMessage: 'Searching...' }); 41 | _this.props.client 42 | .query({ 43 | query: gql` 44 | { 45 | getUserInfo(username:"${username}") { 46 | userName 47 | id 48 | role 49 | } 50 | } 51 | ` 52 | }).then(res => { 53 | let state = {users: res.data.getUserInfo}; 54 | if (!res.data.getUserInfo.length) { 55 | state.waitingMessage = 'User not found'; 56 | } 57 | this.setState(state); 58 | }); 59 | } 60 | }); 61 | } 62 | 63 | updateRole = async () => { 64 | if(!this.state.role.length){ 65 | this.setState({ 66 | valmsg: 'Select role' 67 | }); 68 | }else { 69 | this.setState({ 70 | alert: true, 71 | preloader: true, 72 | preloaderMsg: 'Assigning role', 73 | }); 74 | await this.props.client 75 | .mutate({ 76 | mutation: gql` 77 | mutation roleUpdate( 78 | $currentRole: String! 79 | $agentAddress: ID! 80 | $newRole: String! 81 | ) { 82 | roleUpdate (currentRole:$currentRole, agentAddress:$agentAddress, newRole:$newRole) 83 | } 84 | `, 85 | variables: { 86 | currentRole: this.state.currentRole, 87 | agentAddress: this.state.agentAddress, 88 | newRole: this.state.role, 89 | } 90 | }).then(res =>{ 91 | this.props.client.resetStore(); 92 | this.unselectUser(); 93 | 94 | this.setState({ 95 | alert: false, 96 | preloader: false, 97 | preloaderMsg: '' 98 | }); 99 | }); 100 | } 101 | } 102 | 103 | unselectUser = ()=>{ 104 | this.setState({ 105 | userSelected: false, 106 | userName: '', 107 | currentRole: '', 108 | agentAddress: '', 109 | users: [], 110 | role: '' 111 | }); 112 | } 113 | 114 | selectUser = (e)=>{ 115 | let el = e.target, 116 | tag = el.nodeName, 117 | pos = el.dataset.pos; 118 | 119 | if (tag === 'SPAN') { pos = el.parentNode.dataset.pos; } 120 | let currentUser = this.state.users[pos]; 121 | this.setState({ 122 | userSelected: true, 123 | userName: currentUser.userName, 124 | currentRole: currentUser.role, 125 | agentAddress: currentUser.id, 126 | users: [] 127 | }); 128 | } 129 | render() { 130 | return ( 131 |
132 | 137 |
138 |
139 | 140 |
141 | 142 |
143 | 144 |
145 |
146 |
e.preventDefault() }> 147 |
148 | { this.setUsername(e) }} 153 | disabled={ 154 | this.state.userSelected ? 155 | true : false} 156 | /> 157 |
158 | 159 |
160 |
161 | { 162 | !this.state.userSelected ? 163 | 164 | : 165 | 166 | } 167 |
168 |
169 | 170 | {this.state.userSelected && 171 | 172 |
173 | 185 |
186 | 187 |
188 | 191 |
192 | 193 |
194 | 197 |
198 |
199 | } 200 |
201 | 202 |
203 | 204 |
205 | {(this.state.userName.length >= 3 && this.state.users.length > 0) && 206 | 207 |
208 | 209 |
210 | 211 |
212 |
    213 | {this.state.users.map((user, key)=>{ 214 | return( 215 |
  • { this.selectUser(e) }}> 216 | {user.userName} 217 | {user.role} 218 |
  • 219 | ) 220 | })} 221 |
222 |
223 |
224 | } 225 | 226 | {(this.state.waitingMessage.length > 0 && 227 | this.state.userName.length >= 3 && 228 | this.state.users.length === 0 && 229 | !this.state.userSelected) && 230 | 231 | } 232 | 233 |
234 |
235 | 236 |
237 |
238 | 239 | 244 |
245 | ) 246 | } 247 | } 248 | 249 | function mapDispatchToProps(dispatch) { 250 | return {}; 251 | } 252 | 253 | function mapStateToProps(state) { 254 | return { 255 | client: state.client, 256 | userId: state.userId 257 | } 258 | } 259 | 260 | export default connect(mapStateToProps, mapDispatchToProps)(RolesManagement) -------------------------------------------------------------------------------- /src/graphql/resolvers.js: -------------------------------------------------------------------------------- 1 | var id; 2 | function roleUpdate(callZome, fn, role, address) { 3 | return callZome('__H_Wiki', 'wiki', fn) 4 | ({role_name: role, agent_address: address}) 5 | } 6 | 7 | export const resolvers = { 8 | Query: { 9 | async page(_, { title }, __) { 10 | return title; 11 | }, 12 | allPages(_, __, { callZome }) { 13 | return callZome('__H_Wiki', 'wiki', 'get_titles') 14 | ({}) 15 | .then(page => { 16 | page = JSON.parse(page); 17 | if (page.Ok) { 18 | return page.Ok; 19 | } else { 20 | return []; 21 | } 22 | }); 23 | }, 24 | allUsers(_, __, { callZome }) { 25 | return callZome('__H_Wiki', 'wiki', 'get_usernames') 26 | ({}) 27 | .then(page => { 28 | page = JSON.parse(page); 29 | if (page.Ok) { 30 | return page.Ok; 31 | } else { 32 | return []; 33 | } 34 | }); 35 | }, 36 | getId(_,__,{ callZome }) { 37 | return callZome('__H_Wiki', 'wiki', 'get_username') 38 | ({}) 39 | .then(res => { 40 | res = JSON.parse(res).Ok; 41 | if (res) { 42 | return res; 43 | } else { 44 | return "" 45 | } 46 | }); 47 | }, 48 | getPageTitle(_,{title}, {callZome}) { 49 | return callZome('__H_Wiki', 'wiki', 'get_titles_filtered') 50 | ({data: title}) 51 | .then(titles => { 52 | return JSON.parse(titles).Ok 53 | }); 54 | }, 55 | getUsername(_,{username}, {callZome}) { 56 | return callZome('__H_Wiki', 'wiki', 'get_users') 57 | ({data: username}) 58 | .then(usernames => { 59 | return JSON.parse(usernames).Ok 60 | }); 61 | }, 62 | getUserInfo(_,{username}, {callZome}) { 63 | return callZome('__H_Wiki', 'wiki', 'get_users') 64 | ({data: username}) 65 | .then(usernames => { 66 | return JSON.parse(usernames).Ok 67 | }); 68 | } 69 | }, 70 | User: { 71 | userName: userName => userName, 72 | id(user_name, __, { callZome }) { 73 | return callZome('__H_Wiki', 'wiki', 'get_agent_user') 74 | ({ user_name }) 75 | .then(page => { 76 | page = JSON.parse(page); 77 | return page.Ok 78 | }); 79 | 80 | }, 81 | role(user_name, __, { callZome }) { 82 | return callZome('__H_Wiki', 'wiki', 'get_agent_user') 83 | ({ user_name }) 84 | .then(page => { 85 | page = JSON.parse(page); 86 | if (page.Ok) { 87 | return callZome('__H_Wiki', 'wiki', 'get_agent_roles') 88 | ({ agent_address: page.Ok }).then((roles) => { 89 | roles = JSON.parse(roles).Ok; 90 | return roles || 'Reader'; 91 | }); 92 | } else { 93 | throw new Error(page.Err); 94 | } 95 | }) 96 | .catch((e) => { 97 | return 'Reader' 98 | }); 99 | } 100 | }, 101 | Role: { 102 | name: ({ role_name }) => role_name, 103 | members({ members }, __, { callZome }) { 104 | return members.map(id => 105 | callZome('__H_Wiki', 'wiki', 'get_user_by_agent_id') 106 | ({ agent_id: id }).then(page => { 107 | page = JSON.parse(page); 108 | if (page.Ok) { 109 | return page.Ok; 110 | } else { 111 | throw new Error(page.Err); 112 | } 113 | }) 114 | ); 115 | } 116 | }, 117 | Page: { 118 | title(title) { 119 | return title; 120 | }, 121 | sections(title, __, { callZome }) { 122 | return callZome('__H_Wiki', 'wiki', 'get_page') 123 | ({ title: title }) 124 | .then(page => { 125 | page = JSON.parse(page); 126 | if (page.Ok) { 127 | return page.Ok.sections; 128 | } else { 129 | throw new Error(page.Err); 130 | } 131 | }); 132 | } 133 | }, 134 | Section: { 135 | id(id) { 136 | return id; 137 | }, 138 | type(id, __, { callZome }) { 139 | return callZome('__H_Wiki', 'wiki', 'get_section') 140 | ({ address: id }).then(data => { 141 | data = JSON.parse(data); 142 | if (data.Ok) { 143 | return data.Ok.type; 144 | } else { 145 | throw new Error(data.Err); 146 | } 147 | }); 148 | }, 149 | content(id, __, { callZome }) { 150 | return callZome('__H_Wiki', 'wiki', 'get_section') 151 | ({ address: id }).then(data => { 152 | data = JSON.parse(data); 153 | if (data.Ok) { 154 | return data.Ok.content; 155 | } else { 156 | throw new Error(data.Err); 157 | } 158 | }); 159 | }, 160 | rendered_content(id, __, { callZome }) { 161 | return callZome('__H_Wiki', 'wiki', 'get_section') 162 | ({ address: id }).then(data => { 163 | data = JSON.parse(data); 164 | if (data.Ok) { 165 | return data.Ok.rendered_content; 166 | } else { 167 | throw new Error(data.Err); 168 | } 169 | }); 170 | } 171 | }, 172 | Mutation: { 173 | async createPageWithSections(a, { title, sections }, { callZome }) { 174 | return callZome('__H_Wiki', 'wiki', 'create_page_with_sections') 175 | ({ title, sections, timestamp:Date.now().toString()}) 176 | .then(res => { 177 | if (JSON.parse(res).Ok) { 178 | 179 | return title; 180 | } else { 181 | throw new Error(JSON.parse(res).Err); 182 | } 183 | }); 184 | }, 185 | 186 | async addSectionToPage(a, { title, section }, { callZome }) { 187 | await callZome('__H_Wiki', 'wiki', 'add_section') 188 | ({ title, section: section }).then(res => { 189 | id = [JSON.parse(res).Ok]; 190 | }); 191 | 192 | return callZome('__H_Wiki', 'wiki', 'update_page') 193 | ({ sections: id, title, timestamp:Date.now().toString() }) 194 | .then(res => { 195 | if (JSON.parse(res).Ok) { 196 | return title; 197 | } else { 198 | throw new Error(JSON.parse(res).Err); 199 | } 200 | }); 201 | }, 202 | 203 | async addOrderedSectionToPage(a, { title, beforeSection, section, sections, mode },{ callZome }) { 204 | await callZome('__H_Wiki', 'wiki', 'add_section') 205 | ({ title, section: section }) 206 | .then(res => { 207 | id = JSON.parse(res).Ok; 208 | }); 209 | if (mode === 'addsa') { 210 | let sectionsUpdate; 211 | sectionsUpdate = [id, ...sections]; 212 | sections = []; 213 | sections = sectionsUpdate; 214 | } else if (mode === 'addsb') { 215 | let i = parseInt(sections.indexOf(beforeSection)); 216 | i += 1; 217 | sections.splice(i, 0, id); 218 | } 219 | 220 | return callZome('__H_Wiki', 'wiki', 'update_page') 221 | ({ sections, title, timestamp: Date.now().toString() }).then(res => { 222 | 223 | if (JSON.parse(res).Ok) { 224 | return title; 225 | } else { 226 | throw new Error(JSON.parse(res).Err); 227 | } 228 | }); 229 | }, 230 | async updateSection(a, { id, section }, { callZome }) { 231 | return callZome('__H_Wiki', 'wiki', 'update_section') 232 | ({ address: id, section: section }) 233 | .then(res => { 234 | if (JSON.parse(res).Ok) { 235 | return id; 236 | } else { 237 | throw new Error(JSON.parse(res).Err); 238 | } 239 | }); 240 | }, 241 | async removeSection(a, { id }, { callZome }) { 242 | return callZome('__H_Wiki', 'wiki', 'delete_section') 243 | ({ address: id }) 244 | .then(res => { 245 | if (JSON.parse(res).Ok) { 246 | return JSON.parse(res).Ok; 247 | } else { 248 | throw new Error(JSON.parse(res).Err); 249 | } 250 | }); 251 | }, 252 | async createUser(a, { name }, { callZome }) { 253 | return callZome('__H_Wiki', 'wiki', 'create_user') 254 | ({ data: name }) 255 | .then(res => { 256 | if (JSON.parse(res).Ok) { 257 | return JSON.parse(res).Ok; 258 | 259 | } else { 260 | throw new Error(JSON.parse(res).Err); 261 | } 262 | }); 263 | }, 264 | async roleUpdate(a, {currentRole, agentAddress, newRole}, { callZome }) { 265 | if (currentRole === 'Reader') { 266 | return roleUpdate(callZome, 'assign_role', newRole, agentAddress) 267 | .then(res => newRole) 268 | } else { 269 | return roleUpdate(callZome, 'unassign_role', currentRole, agentAddress) 270 | .then(e => e) 271 | .then(res =>{ 272 | if (newRole !== 'Reader') { 273 | return roleUpdate(callZome, 'assign_role', newRole, agentAddress) 274 | .then(res => newRole) 275 | } else { 276 | return true; 277 | } 278 | }) 279 | .catch(res => currentRole); 280 | } 281 | } 282 | } 283 | }; 284 | -------------------------------------------------------------------------------- /src/styles/Editor.scss: -------------------------------------------------------------------------------- 1 | div#editor{ 2 | @include modalPropertys(25000); 3 | background-color: oPrimaryColor(.3); 4 | overflow-y: auto; 5 | 6 | >div:not(#alert) { 7 | width: 65%; 8 | background-color: #FAFAFA; 9 | border-radius: .5em; 10 | position: relative; 11 | 12 | >div#alert { 13 | background-color: rgba(255,255,255,.5); 14 | width: 100%; 15 | height: 100%; 16 | } 17 | 18 | >div#rmel-container { 19 | position: relative; 20 | height: 100%; 21 | display: flex; 22 | flex-direction: column; 23 | 24 | >header:first-child { 25 | border-top-left-radius: .5em; 26 | border-top-right-radius: .5em; 27 | height: 50px; 28 | min-height: 50px; 29 | background: #FAFAFA; 30 | 31 | display: flex; 32 | box-shadow: 0px -1px 1px rgb(226, 226, 226) inset; 33 | 34 | >div{ 35 | width: 50%; 36 | 37 | &:first-child{ 38 | display: flex; 39 | align-items: center; 40 | >label{ 41 | text-indent: 1em; 42 | color: #626262; 43 | font-size: 15px; 44 | } 45 | } 46 | 47 | &:last-child{ 48 | display: flex; 49 | justify-content: flex-end; 50 | >div:first-child{ 51 | display: flex; 52 | align-items: center; 53 | >label { 54 | margin-right: .4em; 55 | color: #626262; 56 | font-size: 12px; 57 | text-indent: .8em; 58 | } 59 | } 60 | >div:last-child{ 61 | padding: 0; 62 | margin:0; 63 | overflow: hidden; 64 | position: relative; 65 | 66 | &:before{ 67 | content: ''; 68 | position: absolute; 69 | top: 35%; 70 | height: 30%; 71 | width: 1px; 72 | background-color: rgb(153, 153, 153); 73 | } 74 | 75 | >select{ 76 | padding: 0 1.5em; 77 | margin:0; 78 | overflow: hidden; 79 | height: 100%; 80 | cursor: pointer; 81 | 82 | border-top-right-radius: .5em; 83 | font-size: 12px; 84 | background: transparent; 85 | border: 0; 86 | 87 | transition: .5s; 88 | 89 | &:focus{ 90 | outline: 0; 91 | } 92 | &:hover{ 93 | background: rgb(245, 245, 245); 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | 101 | >div:nth-child(2){ 102 | height: 100%; 103 | 104 | >div.rc-md-editor { 105 | max-height: 618.281px!important; 106 | >div:first-child.rc-md-navigation{ 107 | background-color: rgb(255, 255, 255); 108 | border-color: #f3f3f3; 109 | } 110 | 111 | >div.editor-container 112 | >section.sec-md 113 | >textarea#textarea{ 114 | padding-top: 30px; 115 | } 116 | } 117 | } 118 | >section{ 119 | // 500 hacia abaj0 120 | @media (max-height: 500px) { 121 | height: 300px; 122 | } 123 | // 124 | @media (min-height: 500px) and (max-height: 700px){ 125 | height: 405px; 126 | } 127 | 128 | // Significa de 500 hacia arriba 129 | @media (min-height: 700px) { 130 | height: 600px; 131 | } 132 | 133 | >div{ 134 | height: 100%; 135 | 136 | &.hw-editor-container{ 137 | 138 | } 139 | 140 | &.hw-upLoadFile-container{ 141 | display: flex; 142 | flex-direction: column; 143 | 144 | >div:first-child{ 145 | padding: 2em 0 1em; 146 | display: flex; 147 | justify-content: center; 148 | align-items: center; 149 | position: relative; 150 | 151 | &::after{ 152 | content: ''; 153 | position: absolute; 154 | height: 1px; 155 | width: 40%; 156 | background-color: oPrimaryColor(.5); 157 | bottom: 0; 158 | } 159 | 160 | >button{ 161 | padding: .65em 1.1em; 162 | border: none; 163 | background-color: transparent; 164 | border-radius: .3em; 165 | display: flex; 166 | justify-content: center; 167 | align-items: center; 168 | cursor: pointer; 169 | color: #FFFFFF; 170 | background-color: $primary-color; 171 | >svg{ 172 | margin-right: .5em; 173 | font-size: 1.5em; 174 | } 175 | &:focus{ 176 | outline: 0; 177 | } 178 | } 179 | } 180 | 181 | >div:last-child{ 182 | height: 100%; 183 | overflow-x: hidden; 184 | overflow-y: auto; 185 | position: relative; 186 | >div{ 187 | 188 | width: 100%; 189 | &:first-child{ 190 | display: flex; 191 | justify-content: center; 192 | position: relative; 193 | z-index: 200; 194 | >img{ 195 | width: 95%; 196 | } 197 | } 198 | 199 | &:last-child{ 200 | height: 100%; 201 | position: absolute; 202 | top: 0; 203 | left: 0; 204 | display: flex; 205 | justify-content: center; 206 | align-items: center; 207 | z-index: 100; 208 | opacity: .5; 209 | >svg{ 210 | opacity: .1; 211 | font-size: 10em; 212 | } 213 | } 214 | } 215 | } 216 | } 217 | } 218 | } 219 | 220 | >footer { 221 | height: 55px; 222 | min-height: 55px; 223 | width: 100%; 224 | display: flex; 225 | justify-content: flex-end; 226 | align-items: center; 227 | >div { 228 | margin-right: 1em; 229 | >button { 230 | &:first-child{ 231 | @include buttonPropertys('contoured', 110px, 30.5px); 232 | transition-duration: .3s; 233 | &:hover { 234 | background-color: oPrimaryColor(.1); 235 | } 236 | } 237 | &:last-child{ 238 | @include buttonPropertys('normal', 110px, 30.5px); 239 | margin: 0 1.5em; 240 | } 241 | } 242 | } 243 | } 244 | 245 | >div.autocomplete-cont{ 246 | position: fixed; 247 | background-color:rgba(0,0,0, .25); 248 | z-index: 1; 249 | //Container 250 | >div{ 251 | display: flex; 252 | justify-content: center; 253 | align-items: center; 254 | height: 100%; 255 | width: 100%; 256 | >div{ 257 | height: 85%; 258 | box-sizing: border-box; 259 | padding: 0 .6em .6em; 260 | background-color: #FAFAFA;; 261 | border-radius: .3em; 262 | width: 65%; 263 | display: flex; 264 | flex-direction: column; 265 | 266 | >div:first-child{ 267 | display: flex; 268 | justify-content: flex-end; 269 | padding: .4em 0 .3em; 270 | >button{ 271 | padding: 0; 272 | margin: 0; 273 | display: flex; 274 | justify-content: center; 275 | align-items: center; 276 | border: none; 277 | background-color: transparent; 278 | >svg{ 279 | color: #4b4b4b; 280 | transition: .5s; 281 | cursor: pointer; 282 | } 283 | &:hover > svg { 284 | color: #FF0000; 285 | } 286 | &:focus{ 287 | outline: 0; 288 | } 289 | } 290 | } 291 | 292 | >div:nth-child(2){ 293 | display: flex; 294 | flex-wrap: nowrap; 295 | >div{ 296 | display: flex; 297 | justify-content: center; 298 | align-items: center; 299 | padding: 0 .5em; 300 | background-color: rgb(241, 241, 241); 301 | 302 | border-top-left-radius: .3em; 303 | border-bottom-left-radius: .3em; 304 | >svg{ 305 | font-size: 1.3em; 306 | color: grey; 307 | } 308 | } 309 | >input{ 310 | display: block; 311 | border:none; 312 | padding: 0; 313 | margin:0; 314 | width: 100%; 315 | font-size: .9em; 316 | padding: .84em .6em; 317 | box-sizing: border-box; 318 | border-top-right-radius: .3em; 319 | border-bottom-right-radius: .3em; 320 | color: #4b4b4b; 321 | &:focus{ 322 | outline: 0; 323 | } 324 | } 325 | } 326 | >div:nth-child(3){ 327 | height: 100%; 328 | overflow-y: auto; 329 | >ul{ 330 | >li{ 331 | padding: .85em 0; 332 | font-size: .93em; 333 | color: #3d3d3d; 334 | list-style-type: none; 335 | border-bottom: 1px solid rgba(122, 122, 122, .2); 336 | text-indent: 1em; 337 | cursor: pointer; 338 | &:hover{ 339 | background-color: rgba(122, 122, 122, .1); 340 | } 341 | } 342 | } 343 | } 344 | } 345 | } 346 | } 347 | 348 | .hidden{ 349 | display: none!important; 350 | } 351 | .show{ 352 | display: block!important; 353 | } 354 | } 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /src/components/Editor.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/alt-text */ 2 | import React, { Fragment } from 'react'; 3 | import MdEditor from 'react-markdown-editor-lite'; 4 | import MarkdownIt from 'markdown-it'; 5 | import { MdClose, 6 | MdFindInPage, 7 | MdPersonPin, 8 | MdImage, 9 | MdFileUpload, 10 | MdPlayCircleFilled, 11 | MdAttachFile } from 'react-icons/md'; 12 | import { connect } from 'react-redux'; 13 | import { gql } from "apollo-boost"; 14 | import Alert from './Alert'; 15 | 16 | class Editor extends React.Component { 17 | mdEditor = null; 18 | mdParser = null; 19 | constructor(props) { 20 | super(props); 21 | this.state = { 22 | data: '', 23 | content: '', 24 | displayAC: 'hidden', 25 | searchAlt: 'page', 26 | textarea: undefined, 27 | matchs: [], 28 | mediaType: undefined, 29 | mediaTypes: [], 30 | image: '', 31 | file: undefined, 32 | src: '', 33 | accept: '', 34 | alert: false, 35 | alertMsg: '', 36 | preloaderMsg: '', 37 | preloader: false, 38 | }; 39 | this.mdParser = new MarkdownIt(); 40 | this.editor = React.createRef(); 41 | 42 | this.autocompleteCont = React.createRef(); 43 | this.input = React.createRef(); 44 | this.imageUploader = React.createRef(); 45 | } 46 | 47 | componentDidMount() { 48 | if (this.props.mode === 'edit' ) { 49 | let section = this.props.getContentSection(this.props.pos, this.props.mode), 50 | mediaType = section.type, 51 | state = {}; 52 | 53 | if (mediaType === 'Text') { 54 | state.content = section.content; 55 | } else { 56 | let structure = new DOMParser().parseFromString(section.rendered_content, 'text/html'), 57 | element = structure.querySelector('body').firstChild, 58 | src; 59 | 60 | if (mediaType === 'File') { 61 | let file = {}; 62 | 63 | src = element.getAttribute('href'); 64 | file.name = element.textContent; 65 | state = {file}; 66 | } else { 67 | src = element.getAttribute('src'); 68 | } 69 | this.setmediaType(mediaType, src) 70 | } 71 | state.mediaTypes = [mediaType]; 72 | this.setState(state); 73 | 74 | } else { 75 | this.setState({ 76 | mediaTypes: [ 77 | 'Text', 78 | 'Image', 79 | 'Video', 80 | 'File' 81 | ] 82 | }); 83 | } 84 | 85 | const editor = this.editor.current; 86 | 87 | editor.addEventListener('click', (e)=>{ 88 | if (e.target.nodeName === 'A') { e.preventDefault(); } 89 | }); 90 | 91 | this.setStyleAutoComplete(); 92 | window.addEventListener('resize', ()=>{ this.setStyleAutoComplete(); }); 93 | 94 | const textarea = editor.querySelector('#textarea'); 95 | this.testTextarea(textarea); 96 | this.setState({ textarea: textarea }); 97 | } 98 | 99 | updatePageSections = () => { 100 | let contents = this.getContents(), 101 | data = { 102 | process: 'update', 103 | mode: this.props.mode, 104 | mediaType: this.state.mediaType || 'Text', 105 | content: contents.content, 106 | renderedContent: contents.renderedContent, 107 | pos: this.props.pos, 108 | currentSection: this.props.getContentSection(this.props.pos, this.props.mode), 109 | timeStamp: parseInt(Date.now()), 110 | file: this.state.file 111 | }; 112 | 113 | if (this.props.mode === 'edit') { 114 | this.props.showConfirmation(data); 115 | } else { 116 | this.props.updatePageSections(data); 117 | } 118 | } 119 | 120 | getContents(){ 121 | let mediaType = this.state.mediaType, 122 | data = {}; 123 | 124 | if (mediaType === 'Text' || !mediaType) { 125 | data.content = this.mdEditor.getMdValue(); 126 | data.renderedContent = this.mdEditor.getHtmlValue(); 127 | } else { 128 | data.content = this.state.file; 129 | switch (mediaType) { 130 | case 'Image': 131 | data.renderedContent = ``; 132 | break; 133 | 134 | case 'Video': 135 | data.renderedContent = ``; 136 | break; 137 | 138 | default: 139 | data.renderedContent = `${this.state.file.name}`; 140 | break; 141 | } 142 | } 143 | return data; 144 | } 145 | 146 | setStyleAutoComplete = () =>{ 147 | if ((this.state.mediaType === 'Text' || !this.state.dataTye) && this.editor.current) { 148 | const textarea = this.editor.current.querySelector('#textarea'), 149 | cordinates = textarea.getBoundingClientRect(), 150 | { width, height, left, top } = cordinates, 151 | style = `width: ${width}px; height: ${height}px; left: ${left}px; top: ${top}px;`; 152 | 153 | this.autocompleteCont 154 | .current.setAttribute('style', style); 155 | } 156 | } 157 | 158 | showAutoComplete(alt) { 159 | this.setState({ 160 | searchAlt: alt, 161 | displayAC: 'show', 162 | data: '', 163 | matchs: [] 164 | }, (_this = this) =>{ 165 | _this.input.current.focus(); 166 | }); 167 | } 168 | 169 | closeAutoComplete = ()=> { 170 | this.setState({ 171 | displayAC: 'hidden' 172 | }); 173 | } 174 | 175 | setRefContent = (e) => { 176 | let iCont, 177 | fCont, 178 | ref = `[${e.target.textContent}]()`, 179 | cursorPos = this.state.posCText, 180 | currentContent = this.mdEditor.getMdValue(), 181 | content = ''; 182 | 183 | // Referencia 184 | if (this.state.searchAlt !== 'page') { 185 | ref = `**@${e.target.textContent}** `; 186 | } 187 | 188 | if(cursorPos === 0) { 189 | content = ref + content; 190 | } else if(cursorPos === content.length) { 191 | content = content + ref; 192 | } else { 193 | iCont = currentContent.substring(0, cursorPos); 194 | fCont = currentContent.substring((cursorPos+1), currentContent.length) 195 | content = iCont + ref + fCont; 196 | } 197 | this.setState({ content }, 198 | (_this = this) => { 199 | _this.closeAutoComplete(); 200 | }); 201 | } 202 | 203 | setmediaType(mediaType, src) { 204 | let accept; 205 | 206 | switch (mediaType) { 207 | case 'Image': 208 | accept = 'image/*'; 209 | break; 210 | 211 | case 'Video': 212 | accept = 'video/*'; 213 | break; 214 | 215 | default: 216 | accept = '*' 217 | break; 218 | } 219 | this.setState({ 220 | mediaType, 221 | accept, 222 | src: src || '', 223 | }); 224 | } 225 | 226 | setData = (e) => { 227 | this.setState({ 228 | data: e.target.value 229 | }, (data=this.state.data)=>{ 230 | let fn = this.state.searchAlt === 'page' ? 231 | `getPageTitle(title:"${data}") ` : 232 | `getUsername(username:"${data}")`; 233 | 234 | if (data.length>=3) { 235 | this.props.client 236 | .query({ 237 | query: gql` { ${fn} } ` 238 | }).then(m => { 239 | let matchs = m.data.getPageTitle || m.data.getUsername; 240 | this.setState({ matchs }); 241 | }) 242 | } 243 | }); 244 | } 245 | 246 | upLoadFile = async (evt) => { 247 | 248 | this.setState({ 249 | alert: true, 250 | preloader: true, 251 | preloaderMsg: 'Uploading ' + this.state.mediaType 252 | }); 253 | 254 | var file = evt.target.files[0] || undefined, 255 | _this = this, 256 | reader = new FileReader(); 257 | 258 | reader.onload = function() { 259 | let state = {}; 260 | 261 | if (_this.state.mediaType !== 'File' && _this.state.mediaType.toLowerCase() !== type) { 262 | let comp = type === 'image' ? 'an ' : 'a '; 263 | 264 | state = { 265 | alertMsg : 'The file is not ' + comp + _this.state.mediaType.toLowerCase(), 266 | preloader : false, 267 | preloaderMsg : '', 268 | }; 269 | } else { 270 | state = { 271 | src: this.result, 272 | file, 273 | alert: false, 274 | preloader: false, 275 | preloaderMsg: '' 276 | }; 277 | } 278 | _this.setState(state); 279 | }; 280 | 281 | if (file) { 282 | var type = file.type.split('/')[0]; 283 | reader.readAsDataURL(file); 284 | } else { 285 | this.setState({ 286 | alert: false, 287 | preloader: false, 288 | preloaderMsg: '' 289 | }); 290 | } 291 | } 292 | 293 | testTextarea(textarea){ 294 | textarea.addEventListener('click', (e)=>{ 295 | this.setState({posCText: this.getCursorPos(textarea).start}); 296 | }); 297 | 298 | textarea.addEventListener('keypress', (e)=>{ 299 | this.setState({ 300 | posCText: this.getCursorPos(textarea).start 301 | }, (_this = this) =>{ 302 | if (e.key === '/' || e.key === '@') { 303 | _this.showAutoComplete(e.key === '/' ? 'page': 'username') 304 | } 305 | }); 306 | }); 307 | } 308 | 309 | getCursorPos = (input) => { 310 | if ("selectionStart" in input && document.activeElement === input) { 311 | return { 312 | start: input.selectionStart, 313 | end: input.selectionEnd 314 | }; 315 | } 316 | else if (input.createTextRange) { 317 | var sel = document.selection.createRange(); 318 | if (sel.parentElement() === input) { 319 | var rng = input.createTextRange(); 320 | rng.moveToBookmark(sel.getBookmark()); 321 | for (var len = 0; rng.compareEndPoints("EndToStart", rng) > 0; rng.moveEnd("character", -1)) { 322 | len++; 323 | } 324 | rng.setEndPoint("StartToStart", input.createTextRange()); 325 | for (var pos = { start: 0, end: len }; rng.compareEndPoints("EndToStart", rng) > 0; rng.moveEnd("character", -1)) { 326 | pos.start++; 327 | pos.end++; 328 | } 329 | return pos; 330 | } 331 | } 332 | return -1; 333 | } 334 | 335 | closeAlert = () => { 336 | this.setState({ alert: false }); 337 | } 338 | 339 | render() { 340 | return ( 341 |
342 |
343 |
344 |
345 |
346 | 347 |
348 |
349 |
350 | 351 |
352 |
353 | 364 |
365 |
366 |
367 |
368 | 369 | {(this.state.mediaType === 'Text' || !this.state.mediaType) && 370 |
371 | this.mdEditor = node} 373 | value={this.state.content} 374 | renderHTML={(text) => this.mdParser.render(text)} 375 | /> 376 |
377 | } 378 | 379 | {(this.state.mediaType !== 'Text' && this.state.mediaType) && 380 |
381 | 382 |
383 | 386 | { this.upLoadFile(e) }} 394 | /> 395 |
396 | 397 |
398 | 399 | {(this.state.mediaType === 'Image') && 400 | 401 |
402 | {this.state.src.length > 0 && 403 | 404 | } 405 |
406 |
407 | 408 |
409 |
410 | } 411 | 412 | {this.state.mediaType === 'Video' && 413 | 414 |
415 | {this.state.src.length > 0 && 416 | 419 | } 420 |
421 |
422 | 423 |
424 |
425 | } 426 | 427 | {this.state.mediaType === 'File' && 428 | 429 |
430 | {this.state.src.length > 0 && 431 | 432 | {this.state.file.name} 433 | 434 | } 435 |
436 |
437 | 438 |
439 |
440 | } 441 | 442 |
443 | 444 |
445 | } 446 | 447 |
448 | 449 |
450 |
451 | 452 | 453 |
454 |
455 | 456 |
457 |
458 |
459 |
460 | 463 |
464 |
465 |
466 | { 467 | this.state.searchAlt === 'page' ? 468 | : 469 | 470 | } 471 |
472 | 478 |
479 |
480 |
    481 | { 482 | this.state.matchs.map((val, key) =>{ 483 | return ( 484 |
  • {val}
  • 485 | ) 486 | }) 487 | } 488 |
489 |
490 |
491 |
492 |
493 | 494 |
495 | 502 | 503 |
504 |
505 | ) 506 | } 507 | } 508 | 509 | function mapDispatchToProps(dispatch) { 510 | return {}; 511 | } 512 | 513 | function mapStateToProps(state) { 514 | return { 515 | client: state.client, 516 | userId: state.userId 517 | } 518 | } 519 | 520 | export default connect(mapStateToProps, mapDispatchToProps)(Editor) -------------------------------------------------------------------------------- /src/components/Wiki.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Page from './Page'; 3 | import Navbar from './Navbar'; 4 | import Editor from './Editor'; 5 | import PreviewSection from './PreviewSection'; 6 | import Alert from './Alert'; 7 | 8 | import { MdCreate, MdAdd, MdClose } from "react-icons/md"; 9 | import { connect } from 'react-redux'; 10 | import { uploadFile, fetchFile } from 'holochain-file-storage'; 11 | 12 | /** Apollo cliente, GraphQL */ 13 | import { gql } from "apollo-boost"; 14 | 15 | class Wiki extends React.Component { 16 | constructor(props) { 17 | super(props); 18 | 19 | this.state = { 20 | existingPages: [], 21 | pages: [], 22 | newData: { 23 | title: '', 24 | sections: [ 25 | ] 26 | }, 27 | statusPreload: '', 28 | pageData: { 29 | title: '', 30 | sections: [], 31 | position: undefined 32 | }, 33 | existingPage: false, 34 | editorSettings: [], 35 | pageDataProcess: {}, 36 | previewData: {}, 37 | clearingCache: false, 38 | //Global variable for content show 39 | showPageManager: false, 40 | showEditor: false, 41 | alert: false, 42 | confirmation: false, 43 | confirmationMsg: 'equis', 44 | preloader: false, 45 | loadingPage: true, 46 | 47 | resolve: null, 48 | rslvUM: null, 49 | scssUploadMedia: null, 50 | refreshing: false 51 | }; 52 | 53 | this.pagesContainer = React.createRef(); 54 | this.pageStructure = ` 55 | title 56 | sections { 57 | id 58 | type 59 | content 60 | rendered_content 61 | } 62 | `; 63 | } 64 | 65 | componentDidMount() { 66 | this.props.client 67 | .query({ 68 | query: gql` 69 | { 70 | allPages { 71 | title 72 | } 73 | } 74 | ` 75 | }) 76 | .then(pages =>{ 77 | pages = pages.data.allPages; 78 | let homePage = { title: 'Homepage' }; 79 | if (!pages.length) { 80 | homePage.renderedContent = 'No pages have been created'; 81 | homePage.links = false; 82 | } else { 83 | homePage.renderedContent = this.linkFormatter(pages); 84 | homePage.links = pages; 85 | } 86 | this.setState({ 87 | pages: [homePage], 88 | loadingPage: false 89 | }); 90 | }) 91 | .catch(e => console.log("object", e)); 92 | } 93 | 94 | refreshLinks = async () => { 95 | if (!this.state.refreshing) { 96 | await this.props.client.resetStore(); 97 | this.setState({ loadingPage: true, refreshing: true }); 98 | var pages = this.stateAssignment(this.state.pages); 99 | await this.props.client 100 | .query({ 101 | query: gql` 102 | { 103 | allPages { 104 | title 105 | } 106 | } 107 | ` 108 | }) 109 | .then(_pages =>{ 110 | _pages = _pages.data.allPages; 111 | let homePage = { title: 'Homepage' }; 112 | if (!_pages.length) { 113 | homePage.renderedContent = 'No pages have been created'; 114 | homePage.links = true; 115 | } else { 116 | homePage.renderedContent = this.linkFormatter(_pages); 117 | homePage.links = _pages; 118 | } 119 | pages.splice(0,1, homePage); 120 | this.setState({ 121 | pages, 122 | loadingPage: false, 123 | refreshing: false 124 | }); 125 | }) 126 | .catch(e => console.log("object", e)); 127 | } 128 | } 129 | 130 | async getMedia(address, typeMedia) { 131 | const file = await fetchFile(this.props.callZome, '__H_Wiki')(address); 132 | var media; 133 | 134 | await new Promise((resolve, reject) =>{ 135 | let reader = new FileReader(); 136 | reader.onload = function() { 137 | resolve(this.result); 138 | } 139 | reader.readAsDataURL(file); 140 | }) 141 | .then(src => { 142 | switch (typeMedia) { 143 | case 'Image': 144 | media = ``; 145 | break; 146 | 147 | case 'Video': 148 | media = ``; 151 | break; 152 | 153 | default: 154 | media = `${file.name}`; 155 | break; 156 | } 157 | }); 158 | return media; 159 | } 160 | 161 | async showPage(e){ 162 | e.preventDefault(); 163 | var page, sections, media; 164 | this.setState({loadingPage: true}) 165 | await this.props.client 166 | .query({ 167 | query: gql` 168 | { 169 | page(title:"${e.target.textContent}") { 170 | ${this.pageStructure} 171 | } 172 | } 173 | ` 174 | }) 175 | .then(_page => { 176 | page = _page; 177 | }).catch(e=>{ 178 | this.setState({ 179 | loadingPage: false 180 | }) 181 | }); 182 | 183 | page = page.data.page; 184 | sections = page.sections; 185 | for (var i in sections) { 186 | if (sections[i].type !== 'Text') { 187 | media = await this.getMedia(sections[i].content, sections[i].type); 188 | page.sections[i].rendered_content = media; 189 | } 190 | } 191 | page.renderedContent = this.sectionContentFormatter(page.sections); 192 | 193 | var pages = this.state.pages; 194 | pages.splice(this.getPagePositionPage(e) + 1, pages.length); 195 | pages = [...pages, page]; 196 | this.setState({ 197 | pages, 198 | loadingPage: false 199 | }, (_this=this) => { 200 | _this.pagesContainer.current.scrollTo(parseInt(Math.random().toString().substring(2, 10)), 0); 201 | }); 202 | } 203 | 204 | uploadMedia = (sections) => { 205 | return new Promise( async (resolve, reject) => { 206 | 207 | if (this.state.rslvUM === null) { 208 | var originResolve = resolve; 209 | await new Promise((resolve, reject) =>{ 210 | this.setState({ rslvUM: originResolve }, function(){ 211 | resolve(true); 212 | }); 213 | }).then(result => { }); 214 | } 215 | 216 | for (var i in sections) { 217 | if (!sections[i].uploaded) { 218 | var section = sections[i]; 219 | break; 220 | } 221 | } 222 | 223 | if (!section) { 224 | resolve(sections); 225 | return; 226 | } 227 | 228 | if (section.type !== "Text") { 229 | const fileAddress = await uploadFile(this.props.callZome, '__H_Wiki')(section.content); 230 | sections[i].content = fileAddress; 231 | sections[i].rendered_content = 'uploaded'; 232 | } 233 | 234 | sections[i].uploaded = true; 235 | if (parseInt(i) === (sections.length-1)) { 236 | this.state.rslvUM(sections); 237 | } else { 238 | this.uploadMedia(sections); 239 | } 240 | }) 241 | } 242 | 243 | storePage = async () => { 244 | this.setState({ 245 | alert: true, 246 | preloader: true, 247 | preloaderMsg: 'storing page' 248 | }); 249 | var sections = []; 250 | await this.uploadMedia(this.state.pageData.sections) 251 | .then(_sections => { 252 | this.setState({ rslvUM:null }); 253 | sections = _sections; 254 | }); 255 | 256 | for (let i in sections) { 257 | delete sections[i].uploaded; 258 | } 259 | 260 | this.state.pageData.sections = sections; 261 | 262 | 263 | await this.props.client 264 | .mutate({ 265 | mutation: gql` 266 | mutation CreatePageWithSections( 267 | $title: String! 268 | $sections: [SectionInput!]! 269 | ) { 270 | createPageWithSections(title: $title, sections: $sections) { 271 | ${this.pageStructure} 272 | } 273 | } 274 | `, 275 | variables: { title: this.state.pageData.title, sections: this.state.pageData.sections } 276 | }) 277 | .then(e => { 278 | var pages = this.stateAssignment(this.state.pages), 279 | links = pages[0].links, 280 | link = [{title: this.state.pageData.title}]; 281 | 282 | if (typeof(links) === 'boolean') { 283 | pages[0].renderedContent = this.linkFormatter(link); 284 | pages[0].links = link; 285 | } else { 286 | pages[0].renderedContent = this.linkFormatter(links.concat(link)); 287 | } 288 | 289 | this.setState({ 290 | pages, 291 | preloader: false, 292 | alert: false 293 | }, (_this=this)=>{ 294 | _this.closePageManager(); 295 | }); 296 | }); 297 | } 298 | 299 | showEditor = (mode, pos = undefined) => { 300 | this.setState({ 301 | editorSettings: [{ 302 | mode: mode, 303 | pos: pos 304 | }] 305 | }, (_this=this)=>{ 306 | _this.setState({ showEditor: true }) 307 | }); 308 | } 309 | 310 | closeEditor(){ 311 | var state = {showEditor: false}; 312 | 313 | if (this.state.alert) { state.alert = false; } 314 | if (this.state.preloader) { state.preloader = false; } 315 | if (this.state.confirmation) { state.confirmation = false; } 316 | 317 | this.setState({ 318 | editorSettings: [] 319 | }, (_this=this)=>{ 320 | _this.setState(state) 321 | }); 322 | } 323 | 324 | showPageManager = () => { 325 | this.setState({ showPageManager: true }); 326 | } 327 | 328 | closePageManager = () => { 329 | this.setState({ showPageManager: false }); 330 | } 331 | 332 | createPage = () =>{ 333 | var _this = this; 334 | this.setState({ 335 | pageData: { 336 | title: '', 337 | sections: [], 338 | position: undefined 339 | }, 340 | existingPage: false, 341 | }, ()=> { 342 | _this.showPageManager(); 343 | }); 344 | } 345 | 346 | showPageData = (pos) => { 347 | var currentPage = this.stateAssignment(this.state.pages[pos]), 348 | _this = this; 349 | currentPage.position = pos; 350 | this.setState({ 351 | pageData: currentPage, 352 | existingPage: true 353 | }, ()=> { 354 | _this.showPageManager(); 355 | }); 356 | } 357 | 358 | getPagePositionPage = (e)=> { 359 | let el = e.target, 360 | parent = el.parentNode, 361 | cont = 2, 362 | pos; 363 | for (var i = 0; i { 375 | let pageData = this.state.pageData; 376 | pageData.title = e.target.value; 377 | this.setState({ pageData: {...pageData} }); 378 | } 379 | 380 | getContentSection = (pos) =>{ 381 | return this.state.pageData.sections[pos]; 382 | } 383 | 384 | stateAssignment(state){ 385 | var newState = JSON.stringify(state); 386 | return JSON.parse(newState); 387 | } 388 | 389 | verifySectionsUpdated = async (param) => { 390 | var title = param.title, 391 | page = { 392 | title: title, 393 | sections: [] 394 | }; 395 | await this.props.client.resetStore(); 396 | return await new Promise((resolve, reject) => { 397 | if (!this.state.resolve) { 398 | this.setState({ resolve }) 399 | } 400 | this.props.client 401 | .query({ 402 | query: gql` 403 | { 404 | page(title:"${title}") { 405 | sections { 406 | id 407 | type 408 | content 409 | rendered_content 410 | } 411 | } 412 | } 413 | ` 414 | }) 415 | .then(res => { 416 | let sections = res.data.page.sections; 417 | if (param.method === 'addns') { 418 | if (sections.length > 0) { 419 | page.sections = sections; 420 | this.state.resolve(page); 421 | } else { 422 | this.verifySectionsUpdated(param); 423 | } 424 | } else if (param.method === 'addsb' || param.method === 'addsa') { 425 | if (sections.length > param.sections.length) { 426 | page.sections = sections; 427 | this.state.resolve(page); 428 | } else { 429 | this.verifySectionsUpdated(param); 430 | } 431 | } 432 | }) 433 | .catch(e => e) 434 | }) 435 | } 436 | 437 | async updatePageSections(data) { 438 | let state = {}, 439 | section = { 440 | type: data.mediaType, 441 | content: data.content, 442 | rendered_content: data.renderedContent, 443 | timestamp: data.timeStamp 444 | }, 445 | pageData = this.state.pageData, 446 | secitonUpdated, 447 | mode = data.mode, 448 | pos = data.pos, 449 | currentSection = data.currentSection; 450 | 451 | if (this.state.existingPage) { 452 | 453 | this.setState({ 454 | alert: true, 455 | preloader: true, 456 | }); 457 | 458 | var pages = this.stateAssignment(this.state.pages); 459 | 460 | if (data.mediaType !== 'Text') { 461 | const fileAddress = await uploadFile(this.props.callZome, '__H_Wiki')(data.file); 462 | data.content = fileAddress; 463 | 464 | section.content = fileAddress; 465 | section.rendered_content = 'uploaded'; 466 | } 467 | 468 | if (mode === 'addns') { 469 | this.setState({preloaderMsg: 'Adding section to the page'}); 470 | await this.props.client 471 | .mutate({ 472 | mutation: gql` 473 | mutation addSectionToPage( 474 | $title: String! 475 | $section: SectionInput! 476 | ) { 477 | addSectionToPage(title: $title, section: $section) { 478 | ${this.pageStructure} 479 | } 480 | } 481 | `, 482 | variables: {title: pageData.title, section: section} 483 | }) 484 | .then(res =>{ }); 485 | } else if (mode === 'addsb' || mode === 'addsa') { 486 | this.setState({preloaderMsg: 'Adding section to the page'}); 487 | var sections = []; 488 | for (let i in pageData.sections) { 489 | sections.push(pageData.sections[i].id); 490 | } 491 | await this.props.client 492 | .mutate({ 493 | mutation: gql` 494 | mutation addOrderedSectionToPage( 495 | $title: String! 496 | $beforeSection: ID! 497 | $section: SectionInput! 498 | $sections: [ID!]!, 499 | $mode: String! 500 | ) { 501 | addOrderedSectionToPage( 502 | title: $title, 503 | beforeSection: $beforeSection, 504 | section: $section, 505 | sections: $sections, 506 | mode: $mode 507 | ) { 508 | ${this.pageStructure} 509 | } 510 | } 511 | `, 512 | variables: { 513 | title: pageData.title, 514 | beforeSection: pageData.sections[pos].id, 515 | section, 516 | sections, 517 | mode 518 | } 519 | }) 520 | .then(res => { }); 521 | } else if (mode === 'edit') { 522 | this.setState({preloaderMsg: 'Updating section to the page'}); 523 | await this.props.client 524 | .mutate({ 525 | mutation: gql` 526 | mutation UpdateSection( 527 | $id: ID! 528 | $section: SectionInput! 529 | ) { 530 | updateSection(id: $id, section: $section) { 531 | id 532 | type 533 | content 534 | rendered_content 535 | } 536 | } 537 | `, 538 | variables: { id: currentSection.id, section: { 539 | type: data.mediaType, 540 | content: data.content, 541 | rendered_content: data.mediaType === 'Text' ? data.renderedContent : 'uploaded', 542 | timestamp: parseInt(Date.now()) 543 | }} 544 | }) 545 | .then(res => { 546 | secitonUpdated = currentSection; 547 | secitonUpdated.type = data.mediaType; 548 | 549 | secitonUpdated.content = data.mediaType === 'Text' ? data.content : data.renderedContent; 550 | 551 | secitonUpdated.rendered_content = data.renderedContent; 552 | 553 | pageData.sections.splice(pos, 1, secitonUpdated); 554 | pageData.renderedContent = this.sectionContentFormatter(pageData.sections); 555 | pages.splice(pageData.position, 1, pageData); 556 | state = { 557 | pageData, 558 | pages 559 | }; 560 | }); 561 | await this.props.client.resetStore(); 562 | } 563 | 564 | if (mode !== 'edit') { 565 | var updatedPage = {}; 566 | await this.verifySectionsUpdated({ 567 | title: pageData.title, 568 | sections: pageData.sections, 569 | method: mode 570 | }).then(_page => { 571 | this.setState({ resolve: null }); 572 | updatedPage = _page; 573 | }).catch(e => e); 574 | 575 | var i_section, j_section, match = false; 576 | 577 | for (var i = 0; i< updatedPage.sections.length; i++ ) { 578 | i_section = updatedPage.sections[i]; 579 | for (var j = 0; j{ 629 | _this.closeEditor(); 630 | }); 631 | } 632 | 633 | async removeSection(pos) { 634 | var pageData = this.stateAssignment(this.state.pageData), 635 | pages = this.stateAssignment(this.state.pages), 636 | state; 637 | 638 | if (this.state.existingPage) { 639 | 640 | this.setState({ 641 | preloader: true, 642 | preloaderMsg: 'removing section' 643 | }); 644 | 645 | await this.props.client 646 | .mutate({ 647 | mutation: gql` 648 | mutation removeSection( 649 | $id: ID! 650 | ) { 651 | removeSection(id: $id) { 652 | ${this.pageStructure} 653 | } 654 | } 655 | `, 656 | variables: { id: pageData.sections[pos].id} 657 | }) 658 | .then(res => { 659 | 660 | pageData.sections.splice(pos, 1); 661 | pageData.renderedContent = this.sectionContentFormatter(pageData.sections); 662 | pages.splice(pageData.position, 1, pageData); 663 | 664 | state = { 665 | pageData, 666 | pages 667 | }; 668 | }); 669 | } else { 670 | pageData.sections.splice(pos, 1); 671 | state = { 672 | pageData 673 | }; 674 | } 675 | 676 | await this.props.client.resetStore(); 677 | 678 | this.setState(state, (_this=this)=>{ 679 | var _state = {alert: false}; 680 | if (_this.state.existingPage) { 681 | _state.preloader = false; 682 | } 683 | _this.setState(_state); 684 | }); 685 | } 686 | 687 | showConfirmation(pageDataProcess) { 688 | this.setState({ 689 | alert: true, 690 | confirmation: true, 691 | confirmationMsg: 'Are you sure you want to '+ pageDataProcess.process +' this?', 692 | pageDataProcess 693 | }); 694 | } 695 | 696 | processPageData = () =>{ 697 | this.setState({ 698 | confirmation: false 699 | }, (_this=this, pageDataProcess = this.state.pageDataProcess)=>{ 700 | if (pageDataProcess.process === 'update') { 701 | _this.updatePageSections(pageDataProcess); 702 | } else if (pageDataProcess.process === 'remove') { 703 | _this.removeSection(pageDataProcess.pos); 704 | } 705 | }); 706 | } 707 | 708 | closeConfirmation = () => { 709 | this.setState({ 710 | alert:false, 711 | confirmation:false, 712 | pageDataProcess: {} 713 | }); 714 | } 715 | 716 | closePreloader = () => { 717 | this.setState({ 718 | alert:false, 719 | preloader:false, 720 | }); 721 | } 722 | 723 | linkFormatter(links){ 724 | let content = '', i; 725 | for(i in links){ 726 | content = content + `
  • ${links[i].title}
  • `; 727 | } 728 | return `
      ${content}
    `; 729 | } 730 | 731 | sectionContentFormatter(sections){ 732 | let content = '', i; 733 | for (i in sections) { 734 | content = content + sections[i].rendered_content; 735 | } 736 | return content; 737 | } 738 | 739 | setRenderContent(content, container, pos = 0) { 740 | let structure = new DOMParser().parseFromString(content, 'text/html'), 741 | cont = document.createElement('div'), 742 | subCont = document.createElement('div'); 743 | 744 | subCont.dataset.page = pos; 745 | subCont.innerHTML = structure.querySelector('body').innerHTML; 746 | cont.appendChild(subCont); 747 | container.innerHTML = cont.innerHTML; 748 | } 749 | 750 | render() { 751 | 752 | return ( 753 |
    754 | 755 | 763 | 764 |
    765 |
    766 | {this.state.pages.map((pageData, key) => { 767 | return ( 768 |
    769 |
    770 | {(key !== 0 && 771 | this.props.userId.role !== 'Reader') && 772 | 775 | } 776 | 777 |
    778 | 784 |
    785 | ) 786 | })} 787 |
    788 | 789 | {this.state.loadingPage &&
    } 790 | 791 |
    792 | 793 | {this.state.showPageManager && 794 |
    795 |
    796 |
    797 |
    798 | 801 |
    802 |
    803 | { 804 | this.state.existingPage && 805 | 808 | } 809 |
    810 |
    811 |
    812 | {!this.state.existingPage && 813 |
    814 | 821 |
    822 | } 823 |
    824 | 825 | {!this.state.pageData.sections.length && 826 | 829 | } 830 | 831 | {this.state.pageData.sections.map((data, key) => { 832 | return( 833 | 842 | ) 843 | })} 844 | 845 |
    846 |
    847 | {!this.state.existingPage && 848 |
    849 |
    850 |
    851 | 852 | 853 |
    854 |
    855 |
    856 | } 857 |
    858 |
    859 | } 860 | 861 | {this.state.showEditor && 862 | this.state.editorSettings.map((param, key) => { 863 | return( 864 | 873 | ) 874 | }) 875 | } 876 | 877 | 889 | 890 |
    891 | ) 892 | } 893 | } 894 | 895 | function mapDispatchToProps(dispatch) { 896 | return {}; 897 | } 898 | 899 | function mapStateToProps(state) { 900 | return { 901 | client: state.client, 902 | userId: state.userId, 903 | callZome: state.callZome 904 | } 905 | } 906 | 907 | export default connect(mapStateToProps, mapDispatchToProps)(Wiki) -------------------------------------------------------------------------------- /src/styles/App.scss: -------------------------------------------------------------------------------- 1 | @import './helpers.scss'; 2 | @import './Editor.scss'; 3 | @import './PreviewSection.scss'; 4 | 5 | div.container{ 6 | display: flex; 7 | flex-direction: column; 8 | height: 100vh; 9 | width: 100vw; 10 | background-color: white; 11 | overflow: hidden; 12 | 13 | > nav{ 14 | height: 7vh; 15 | width: 100%; 16 | display: flex; 17 | background-color: #EBECF1;//$primary-color; 18 | box-shadow: rgba(128,128,128, .2) 0px 10px 10px; 19 | position: relative; 20 | z-index: 10000; 21 | 22 | > div:first-child, > div:nth-child(2) { 23 | width: 50%; 24 | height: 100%; 25 | display: flex; 26 | } 27 | 28 | > div:first-child { 29 | align-items: center; 30 | > div { 31 | height: 6vh; 32 | width: 10vw; 33 | background-image: url('/LogoH-wik-8.png'); 34 | 35 | background-position: center; 36 | background-size:63%; 37 | background-repeat: no-repeat; 38 | cursor: pointer; 39 | } 40 | } 41 | 42 | > div:nth-child(2) { 43 | display: flex; 44 | justify-content: flex-end; 45 | align-items: center; 46 | >ul { 47 | display: flex; 48 | align-items: center; 49 | flex-wrap: nowrap; 50 | >li { 51 | list-style: none; 52 | margin-right: 19px; 53 | position: relative; 54 | transition: .5s; 55 | 56 | >a{ 57 | text-decoration: none; 58 | color: rgba(100,100,100, 1); 59 | padding: .2em 0; 60 | transition: .5s; 61 | opacity: .68; 62 | &>svg{ 63 | color: $primary-color; 64 | font-size: 1.3em; 65 | } 66 | &:hover { 67 | opacity: 1; 68 | } 69 | } 70 | 71 | >button{ 72 | @include buttonPropertys('normal'); 73 | border-radius: .25em; 74 | padding: .5em 1em .5em .6em; 75 | &>svg{ 76 | color: #FFFFFF; 77 | } 78 | } 79 | 80 | >a, >button{ 81 | display: flex; 82 | align-items: center; 83 | font-size: .875em; 84 | &>svg{ 85 | margin-right: .35em; 86 | } 87 | } 88 | 89 | &:not(:first-child)::before{ 90 | content: ''; 91 | position: absolute; 92 | height: 140%; 93 | width: 1px; 94 | background-color: #dfdfdf; 95 | left: -10px; 96 | top: 50%; 97 | transform: translateY(-50%); 98 | } 99 | 100 | &:not(:last-child){ 101 | padding: 0em 1em; 102 | } 103 | &:last-child{ 104 | padding-left: 1em; 105 | } 106 | } 107 | } 108 | } 109 | 110 | > div:last-child.linear-preloader { 111 | position: absolute; 112 | left: 0; 113 | height: 3px; 114 | } 115 | } 116 | 117 | > section { 118 | width: inherit; 119 | height: 93vh; 120 | 121 | overflow-x: auto; 122 | overflow-y: hidden; 123 | background-color: #F0F1F4; 124 | position: relative; 125 | } 126 | 127 | /** 128 | * HOMEPAGE 129 | */ 130 | > section.pages-container{ 131 | 132 | > div{ 133 | height: 100%; 134 | display: flex; 135 | flex-wrap: nowrap; 136 | 137 | > div { //Los que contienen data-page 138 | margin: 25px 15px 15px; 139 | min-width: 480px!important; 140 | width: 480px!important; 141 | max-width: 480px!important; 142 | position: relative; 143 | background-color: #FAFAFA; 144 | border-radius: .4em; 145 | // box-shadow: #EBECF1 1px 1px 0px, #EBECF1 -1px -1px 0; 146 | border: 1px solid #EBECF1; 147 | box-sizing: border-box; 148 | overflow-x: hidden; 149 | overflow-y: scroll; 150 | 151 | //Contenedor del boton editar 152 | >div:first-child { 153 | position: absolute; 154 | right: 2px; 155 | top: 8px; 156 | >button{ 157 | border: none; 158 | background-color: transparent; 159 | cursor: pointer; 160 | outline: 0; 161 | position: relative; 162 | z-index: 1000; 163 | >svg{ 164 | font-size: 1.7em; 165 | color: #54525a; 166 | transition: .5s; 167 | } 168 | &:hover > svg { 169 | color: $primary-color; 170 | } 171 | } 172 | } 173 | // Ocultar el boton editar del Homepage 174 | &:first-child>div:first-child{ display: none; } 175 | &:first-child{ 176 | >article>div { 177 | > div{ 178 | > ul:first-child{ 179 | >li { 180 | margin: 0.5em 0 .9em 0; 181 | list-style: none; 182 | position: relative; 183 | &::before{ 184 | content: '.'; 185 | color: transparent; 186 | position: absolute; 187 | top: 50%; 188 | transform: translateY(-50%); 189 | left:0; 190 | min-width: 16px; 191 | height: 16px; 192 | background : { 193 | image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAyVBMVEUAAABetY1dto4A//9dto5etY5ttpJdtY5dto5eto9ctY9ftZBeto1Vv5Vdto5dto1dto9Av4BhtpJVqoBdto5dto1bto1cuI9Vu4hatI9ZsZBdtI9dto1dto5dto5dto5dto9dt45dto9dto5eto9dt49dtY5dto5cto5cto1cto5eto9dto5dto5dto9dtY5dto5dto5dto9dto5dto5dt41eto5duYtcto5ctY5atYxcto9et4xeto5dt41dto5gv4Bdto4AAAB74QIeAAAAQXRSTlMAgMsB+7QHg/5UZD5XDJrKhAQVBvOdOBkPIhcp0+fd8e6iod9tZt7geneg5feyqrv9l8PymWCeIc9hH3Q80lX8CCHgkNAAAAABYktHRACIBR1IAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAB3RJTUUH5AMMFRQGkVclpwAAANdJREFUOMu1k+cOgjAURqviBnHgQgUFHIjixj3u+7+UrIClxUQTzs9+J71fOhDySaUz8AGTzSGMPAAqfBpQjOZRoRTNCaHMclhOCBXg/bzq1aoRAtTdvCG4C80WEELb26HTtRF7/ThBHEg2A4A4oQ1UfhbkYYhMEUbYwSqkoGrjEG2SRMnpTA+ZqaSgYCXnlBGGFGL802GxNB1W6zhh40/fxgk7Ze9gMgne5hehThcOVvAmeVouHE+BwLFjgvPlCoFgc9tHsO6ACQ9qjWfw99Ary1Bq6HbyBmJhpzVCat+iAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIwLTAzLTEyVDIxOjIwOjA2KzAwOjAwqIifqAAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMC0wMy0xMlQyMToyMDowNiswMDowMNnVJxQAAAAZdEVYdFNvZnR3YXJlAHd3dy5pbmtzY2FwZS5vcmeb7jwaAAAAAElFTkSuQmCC'); 194 | size: 16px; 195 | repeat: no-repeat; 196 | position: center; 197 | }; 198 | } 199 | > a { 200 | color: rgb(59, 59, 59); 201 | margin-left: 25px; 202 | text-decoration: none; 203 | transition: .35s; 204 | 205 | &:hover { 206 | margin-left: 30px; 207 | text-decoration: underline; 208 | text-decoration-color: $primary-color; 209 | } 210 | } 211 | } 212 | } 213 | } 214 | } 215 | 216 | } 217 | // Articulo -> page 218 | >article{ 219 | padding: 0 1.7em; 220 | 221 | >header:first-child{ 222 | padding: 1.2em 0; 223 | display: flex; 224 | align-items: center; 225 | //Conten circulo & h1 title 226 | >div{ 227 | position: relative; 228 | top: 2px; 229 | display: flex; 230 | align-items: center; 231 | >div:first-child{ 232 | min-width: 32px; 233 | width: 32px; 234 | max-width: 32px; 235 | min-height: 32px; 236 | height: 32px; 237 | max-height: 32px; 238 | background-color: oPrimaryColor(0.75); 239 | border-radius: 50%; 240 | margin: 0 .8em 0 0; 241 | } 242 | >h1 { 243 | font-size: 1.08555em; 244 | position: relative; 245 | } 246 | } 247 | } 248 | } 249 | } 250 | } 251 | 252 | >div.blocker{ 253 | position: absolute; 254 | top: 0; 255 | left: 0; 256 | width: 100%; 257 | height: 100%; 258 | background-color: transparent; 259 | cursor: wait; 260 | } 261 | } 262 | 263 | /** 264 | * User registry 265 | */ 266 | > section.user-registry-container{ 267 | display: flex; 268 | justify-content: center; 269 | align-items: center; 270 | > div { 271 | position: relative; 272 | top: -130px; 273 | width: 260px; 274 | 275 | display: flex; 276 | flex-direction: column; 277 | 278 | >div{ 279 | display: flex; 280 | flex-wrap: wrap; 281 | justify-content: center; 282 | 283 | >div:first-child{ 284 | border-radius: 50%; 285 | border: 1px solid oPrimaryColor(.5);; 286 | width: 100%; 287 | height: 100px; 288 | width: 100px; 289 | display: flex; 290 | justify-content: center; 291 | align-items: center; 292 | 293 | >svg { 294 | font-size: 4em; 295 | color: $primary-color; 296 | } 297 | } 298 | >div:last-child{ 299 | width: 100%; 300 | display: flex; 301 | justify-content: center; 302 | padding: .9em 0; 303 | >label { 304 | color: #646464; 305 | font-size: 1.27em; 306 | } 307 | } 308 | } 309 | 310 | >form { 311 | display: flex; 312 | flex-wrap: wrap; 313 | justify-content: center; 314 | >div{ 315 | width: 100%; 316 | 317 | &:first-child{ 318 | display: flex; 319 | flex-direction: column; 320 | >div{ 321 | display: flex; 322 | justify-content: center; 323 | min-height: .5em; 324 | >label{ 325 | padding: .3em 0 .5em; 326 | color: rgb(255, 100, 100); 327 | font-size: .75em; 328 | } 329 | } 330 | } 331 | 332 | >input{ 333 | width: inherit; 334 | margin: 0!important; 335 | border: 0; 336 | font-size: .99em; 337 | padding: .6em 0; 338 | text-align: center; 339 | box-sizing: border-box; 340 | border-radius: .3em; 341 | color: #616161; 342 | 343 | &::placeholder{ opacity: .4; } 344 | &:focus{ 345 | outline: 0; 346 | box-shadow: 0 0 0 1px oPrimaryColor(.4); 347 | } 348 | } 349 | 350 | >button { 351 | @include buttonPropertys('normal', 100%, auto); 352 | font-size: .9em; 353 | padding: .5em 0; 354 | } 355 | } 356 | } 357 | } 358 | } 359 | 360 | /** 361 | * Roles managment 362 | */ 363 | >section.roles-managment-container{ 364 | display: flex; 365 | justify-content: center; 366 | align-items: center; 367 | 368 | >div{ 369 | width:750px; 370 | height: 90%; 371 | background-color: #FAFAFA; 372 | box-sizing: border-box; 373 | padding: 1em 0; 374 | border-radius: .5em; 375 | display: flex; 376 | flex-direction: column; 377 | 378 | >header{ 379 | display: flex; 380 | align-items: center; 381 | height: 10%; 382 | box-sizing: border-box; 383 | padding: 0 1em; 384 | 385 | >label{ 386 | display: flex; 387 | align-items: center; 388 | position: relative; 389 | top: -10px; 390 | color: rgb(73, 73, 73); 391 | font-size: 1.1em; 392 | font-weight: bold; 393 | >svg { 394 | margin-right: .2em; 395 | font-size: 1.35em; 396 | } 397 | } 398 | } 399 | 400 | >section{ 401 | display:flex; 402 | flex-direction: column; 403 | height: 100%; 404 | 405 | >div:first-child{ 406 | position: relative; 407 | box-sizing: border-box; 408 | padding: 0 1em; 409 | 410 | >form{ 411 | display: flex; 412 | justify-content: space-between; 413 | >div{ 414 | display: flex; 415 | align-items: center; 416 | 417 | >input, >select, >button{ 418 | margin: 0; 419 | padding: 0; 420 | width: 100%; 421 | padding: .55em; 422 | border-radius: .3em; 423 | font-size: .86em!important; 424 | &:focus{ 425 | outline: 0; 426 | } 427 | } 428 | 429 | &:first-child{ 430 | width: 100%; 431 | position: relative; 432 | 433 | >input{ 434 | text-indent: 35px; 435 | padding-left: 0; 436 | } 437 | 438 | >div:nth-child(2){ 439 | position: absolute; 440 | top: 100%; 441 | left: 0; 442 | padding: 0; 443 | margin: 0; 444 | 445 | >label { 446 | color: rgb(255, 100, 100); 447 | font-size: .75em; 448 | } 449 | } 450 | 451 | >div:last-child{ 452 | width: 35px; 453 | height: 100%; 454 | display: flex; 455 | justify-content: center; 456 | align-items: center; 457 | position: absolute; 458 | top: 0; 459 | left: 0; 460 | >svg{ 461 | font-size: 1.4em; 462 | color: rgb(117, 117, 117); 463 | } 464 | } 465 | } 466 | 467 | &:nth-child(2){ 468 | padding: 0 .5em; 469 | width: calc(50% - 1em); 470 | } 471 | &:nth-child(3){ 472 | width: 45%; 473 | >button{ 474 | @include buttonPropertys('normal', 100%, 100%); 475 | } 476 | } 477 | 478 | &:nth-child(4){ 479 | min-width: 45px; 480 | justify-content: center; 481 | >button{ 482 | display: flex; 483 | justify-content: center; 484 | width: auto!important; 485 | padding: 0; 486 | margin: 0; 487 | cursor: normal; 488 | border: none; 489 | background-color: transparent; 490 | >svg { 491 | color: rgb(0, 133, 211); 492 | cursor: pointer; 493 | font-size: 1.6em; 494 | } 495 | } 496 | } 497 | 498 | >select, input{ 499 | background-color: #FFFFFF; 500 | border-radius: .5em; 501 | border: 1px solid rgba(70, 70, 70, .3); 502 | &::placeholder{ color: rgb(124, 124, 124); } 503 | &:focus{ 504 | outline: 0; 505 | box-shadow: 0 0 0 1px oPrimaryColor(.4); 506 | } 507 | } 508 | } 509 | } 510 | } 511 | 512 | >div:last-child{ 513 | height: 100%; 514 | display:flex; 515 | flex-direction: column; 516 | 517 | // Content label for result and eraser button 518 | >div:first-child{ 519 | 520 | position: relative; 521 | z-index: 1000; 522 | padding: 1.75em 0 .5em 1em; 523 | box-shadow: rgba(128,128,128, .1) 0px 3px 2px; 524 | >label{ 525 | font-size: .9em; 526 | color: #4e4e4e; 527 | font-weight: bold; 528 | } 529 | } 530 | 531 | //Result content 532 | >div:last-child{ 533 | position: relative; 534 | z-index: 500; 535 | height: 100%; 536 | overflow-x:hidden; 537 | overflow-y: auto; 538 | li { 539 | color: #666666; 540 | list-style: none; 541 | font-size: 1em; 542 | cursor: pointer; 543 | padding: 1em 1.2em; 544 | border-radius: .2em; 545 | 546 | >span{ 547 | padding-top: .285em; 548 | display: block; 549 | font-size: .8em; 550 | } 551 | &:not(:last-child){ 552 | border-bottom: 1px solid rgba(128,128,128, .2); 553 | } 554 | 555 | &:hover{ 556 | background-color: rgba(221, 221, 221, 0.1); 557 | } 558 | } 559 | } 560 | >label{ 561 | padding: .2em 0 .5em 1em; 562 | font-size: .9em; 563 | color: #4e4e4e; 564 | } 565 | } 566 | } 567 | } 568 | } 569 | 570 | div.visual-content{ 571 | >div{ 572 | >p { 573 | font-size: 14px; 574 | line-height: 1.7; 575 | margin: 8px 0; 576 | } 577 | blockquote { 578 | position: relative; 579 | margin: 16px 0; 580 | padding: 5px 8px 5px 30px; 581 | background: none repeat scroll 0 0 rgba(102, 128, 153, 0.05); 582 | border: none; 583 | color: #333; 584 | border-left: 10px solid #D6DBDF; 585 | p { 586 | font-size: 14px; 587 | line-height: 1.7; 588 | margin: 8px 0; 589 | } 590 | } 591 | 592 | h2 { font-size: 24px; 593 | padding: 0px 0; 594 | border: none; 595 | font-weight: 700; 596 | margin: 24px 0; 597 | line-height: 1.7; 598 | } 599 | 600 | code { 601 | background-color: #f5f5f5; 602 | border-radius: 0; 603 | padding: 3px 0; 604 | margin: 0; 605 | font-size: 14px; 606 | overflow-x: auto; 607 | word-break: normal; 608 | } 609 | 610 | pre { 611 | display: block; 612 | background-color: #f5f5f5; 613 | padding: 20px; 614 | font-size: 14px; 615 | line-height: 28px; 616 | border-radius: 0; 617 | overflow-x: auto; 618 | word-break: break-word; 619 | } 620 | 621 | hr { 622 | margin-top: 20px; 623 | margin-bottom: 20px; 624 | border: 0; 625 | border-top: 1px solid #eee; 626 | } 627 | 628 | table { 629 | font-size: 14px; 630 | line-height: 1.7; 631 | max-width: 100%; 632 | overflow: auto; 633 | border: 1px solid #f6f6f6; 634 | border-collapse: collapse; 635 | border-spacing: 0; 636 | -webkit-box-sizing: border-box; 637 | box-sizing: border-box; 638 | 639 | tr { 640 | border: 1px solid #efefef; 641 | } 642 | 643 | tr:nth-child(2n) { 644 | background-color: transparent; 645 | } 646 | 647 | td, th { 648 | word-break: break-all; 649 | word-wrap: break-word; 650 | white-space: normal; 651 | } 652 | 653 | th { 654 | text-align: center; 655 | font-weight: 700; 656 | border: 1px solid #efefef; 657 | padding: 10px 6px; 658 | background-color: #f5f7fa; 659 | word-break: break-word; 660 | } 661 | 662 | td { 663 | border: 1px solid #efefef; 664 | text-align: left; 665 | padding: 10px 15px; 666 | word-break: break-word; 667 | min-width: 60px; 668 | } 669 | 670 | } 671 | 672 | video, img { 673 | width: 100%; 674 | } 675 | } 676 | } 677 | 678 | } 679 | 680 | 681 | 682 | div#page-manager{ 683 | @include modalPropertys(15000); 684 | > div { 685 | width: 65%; 686 | height: 90%; 687 | background-color: $bg2; 688 | display: flex; 689 | flex-direction: column; 690 | border-radius: .5em; 691 | 692 | > header { 693 | height: 68px; 694 | display: flex; 695 | flex-wrap: nowrap; 696 | border-bottom: #EBECF1 1px solid; 697 | >div{ 698 | display: flex; 699 | align-items: center; 700 | } 701 | >div:first-child { 702 | width: 100%; 703 | >label { 704 | color: rgb(122, 122, 122); 705 | font-size: 21.5px; 706 | text-indent: .8em; 707 | } 708 | } 709 | >div:last-child{ 710 | min-width: 70px; 711 | justify-content: center; 712 | >button{ 713 | @include buttonPropertys('contoured'); 714 | padding: .2em; 715 | display: flex; 716 | align-items: center; 717 | font-size: 1.3em; 718 | border: none; 719 | transition-duration: .3s; 720 | &:hover{ 721 | background-color: oPrimaryColor(.1); 722 | } 723 | } 724 | } 725 | } 726 | 727 | > section { 728 | overflow-y: auto; 729 | height: 100%; 730 | display: flex; 731 | flex-direction: column; 732 | 733 | >div:first-child{ 734 | box-sizing: border-box; 735 | padding: 1em; 736 | 737 | >input { 738 | width: 100%; 739 | height: 44px; 740 | background-color: $bg2; 741 | border-radius: .5em; 742 | border: rgba(122,122,122, .25) solid 1px; 743 | 744 | text-indent: .8em; 745 | font-size: .98em; 746 | 747 | &::placeholder{ color: #ddd; } 748 | 749 | &:focus{ 750 | outline: 0; 751 | box-shadow: 0 0 0 1px oPrimaryColor(.4); 752 | } 753 | } 754 | } 755 | 756 | >div:last-child{ 757 | width: 100%; 758 | height: 100%; 759 | overflow-y: scroll; 760 | overflow-x: hidden; 761 | padding: 0 1em; 762 | box-sizing: border-box; 763 | >button { 764 | @include buttonPropertys('normal', 120px, 32px); 765 | display: flex; 766 | justify-content: center; 767 | align-items: center; 768 | font-weight: bold; 769 | > svg { 770 | font-size: 1.3em; 771 | margin-right: .2em; 772 | font-weight: bold; 773 | } 774 | } 775 | } 776 | } 777 | 778 | > footer { 779 | height: 60px; 780 | width: 100%; 781 | display: flex; 782 | align-items: center; 783 | >div { 784 | width: 100%; 785 | >div{ 786 | display: flex; 787 | justify-content: flex-end; 788 | >button { 789 | &:first-child{ 790 | @include buttonPropertys('contoured', 120px, 33px); 791 | transition-duration: .3s; 792 | &:hover { 793 | background-color: oPrimaryColor(.1); 794 | } 795 | } 796 | &:last-child{ 797 | @include buttonPropertys('normal', 120px, 33px); 798 | margin: 0 1.5em; 799 | } 800 | } 801 | } 802 | } 803 | } 804 | } 805 | } 806 | 807 | --------------------------------------------------------------------------------