├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── LICENSE ├── README.md ├── SECURITY.md ├── deploy ├── README.md └── deploy └── www.phantom ├── .browserlistrc ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.png ├── font │ ├── opensans-bold.woff2 │ └── opensans-regular.woff2 ├── img │ ├── card-temporary.png │ └── posts.png ├── index.html └── theme │ ├── banana │ ├── banner.png │ ├── readme.md │ ├── style.css │ └── theme.json │ ├── dark │ ├── banner.png │ ├── style.css │ └── theme.json │ ├── light │ ├── banner.png │ ├── style.css │ └── theme.json │ ├── shared │ ├── default.css │ ├── main.js │ ├── template.html │ ├── theme.json │ └── vue.js │ └── zen │ ├── banner.png │ ├── i18n │ ├── en_GB.json │ └── es_ES.json │ ├── script.js │ ├── style.css │ ├── template.html │ └── theme.json ├── src ├── App.vue ├── component │ ├── DocumentEdit.vue │ ├── DocumentList.vue │ ├── Foot.vue │ ├── Hamburger.vue │ ├── HomeMenu.vue │ ├── ImageGallery.vue │ ├── LanguageSelector.vue │ ├── Loader.vue │ ├── Logo.vue │ ├── Modal.vue │ ├── PageTitleWithActions.vue │ └── Sidebar.vue ├── i18n │ ├── en_GB.js │ ├── es_ES.js │ └── nl_NL.js ├── main.js ├── router.js ├── service │ ├── log │ │ └── logger.js │ ├── markdown │ │ ├── canonical.js │ │ └── formatter.js │ ├── plugin │ │ └── phantom.js │ ├── safe │ │ ├── api.js │ │ ├── cache │ │ │ └── cache.js │ │ ├── lib │ │ │ ├── auth.js │ │ │ ├── files.js │ │ │ └── nrs.js │ │ ├── mock │ │ │ └── safe.js │ │ └── promise.js │ └── theme │ │ ├── importer.js │ │ ├── readme.md │ │ └── theme.js └── view │ ├── About.vue │ ├── Dashboard.vue │ ├── Dev │ └── Log.vue │ ├── Docs │ ├── Home.vue │ └── Wrapper.vue │ ├── DomainCreate.vue │ ├── Domains.vue │ ├── Home.vue │ ├── PageEdit.vue │ ├── Pages.vue │ ├── PostEdit.vue │ ├── Posts.vue │ ├── Theme.vue │ └── Themes.vue └── vue.config.js /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Give us information about unexpected behaviour in the Phantom web app 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: SAFEShane 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** / delete if not applicable 27 | - OS: [e.g. iOS] 28 | - SAFE Browser Version [e.g. 1.16.0-alpha2] 29 | 30 | **Smartphone (please complete the following information):** / delete if not applicable 31 | - Device: [e.g. iPhone6] 32 | - OS: [e.g. iOS8.1] 33 | - SAFE Browser Version [e.g. 1.16.0-alpha2] 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: enhancement 6 | assignees: SAFEShane 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Dependencies** 20 | A list of external dependencies which could hamper this feature, for instance: an outstanding RFC for SAFE browser features, or any libraries we may need to install. 21 | 22 | **Additional context** 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | www.phantom/node_modules/ 2 | www.phantom/dist/ 3 | 4 | # local env files 5 | www.phantom/.env.local 6 | 7 | # Log files 8 | www.phantom/npm-debug.log* 9 | www.phantom/yarn-debug.log* 10 | www.phantom/yarn-error.log* 11 | 12 | # Editor directories and files 13 | .idea 14 | .vscode 15 | *.suo 16 | *.ntvs* 17 | *.njsproj 18 | *.sln 19 | *.sw? 20 | 21 | # MAC OS directory information 22 | .DS_Store -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at shane@helldritch.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We accept all contributions in the form of pull requests targeted at the master branch. When creating a pull request please include a link to the issue it resolves or implements. 4 | 5 | ### Dependencies 6 | 7 | With regards to the Phantom application, we have as few dependencies as possible. If there is a package which provides some functionality, ask yourself if you could implement it yourself more tersely. Unless it is something _fundamental_ like a routing library, we generally want to create it ourselves. 8 | 9 | With regards to the outputted websites, we have zero dependencies, and zero additional dependencies will be tolerated. 10 | 11 | Development only dependencies are welcome, since they only impact the local `node_modules` directory. 12 | 13 | #### Why? Isn't this just "not invented here" syndrome? 14 | 15 | No, it's because the final size of our distributed files is incredibly important. Download speed / download size / the file count can hugely impact performance on the SAFE network. A dependency might only add 3KB of size to the distributed files, but if we can implement it in 2.6KB, you bet your ass we're going to reinvent the wheel. 16 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Phantom contributors 2 | 3 | * Shane Armstrong 4 | * Creator of Phantom 5 | 6 | * Digipl 7 | * Translated Phantom to es_ES 8 | * [SAFE Network Forum page](https://safenetforum.org/u/digipl) 9 | 10 | * Sam1 / Isntism 11 | * Translated Phantom to nl_NL 12 | * [SAFE Network forum page for sam1](https://safenetforum.org/u/sam1) 13 | * [SAFE Network forum page for isntism](https://safenetforum.org/u/isntism) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 SAFE Publishing 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Phantom 2 | 3 | A publishing tool enabling users of the SAFE Network to easily manage websites. Phantom is designed to live entirely within the SAFE network, allowing users to create and manage websites and blogs without ever being exposed to the clearnet. 4 | 5 | This project is MIT licensed, feel free to deploy your own version of Phantom to a SAFE network public name (The SAFE version of DNS) you own if you want a customised Phantom platform. 6 | 7 | ![An image showing the Phantom application](https://github.com/SAFEPublishing/Phantom/blob/master/www.phantom/public/img/posts.png) 8 | 9 | ### Development 10 | 11 | Check the [www.phantom](https://github.com/SAFEPublishing/Phantom/tree/master/www.phantom) directory for information about development. 12 | 13 | ### Deployment 14 | 15 | Check the [Deploy](https://github.com/SAFEPublishing/Phantom/tree/master/deploy) directory for information about deploying. 16 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 0.x.x | :white_check_mark: | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | When reporting a vulnerability which effects any of the following: 15 | 16 | * Privacy of end users 17 | * Security of end users 18 | * Access to secure assets (documents, etc, which are owned by the app) 19 | 20 | Please send your report directly to shane@helldritch.com, and I will create an issue. You will receive full (public) credit for the vulnerability once the patch hits production and all major public versions of Phantom are updated. 21 | 22 | If your vulnerability doesn't effect any of those key issues, please create a standard issue within this repository in Github. _If the issue doesn't effect any of those key issues, but you feel the severity is such that it needs to be patched before it is made public, you can still contact us directly via email at your discretion._ 23 | 24 | We will never mock, ignore or otherwise mistreat you for taking an issue seriously, even if we don't feel it warranted secret disclosure. Ultimately, the security and privacy of our users is our prime concern. 25 | 26 | ## Responsible disclosure policy 27 | 28 | Please note that in the event of a CVE or other issue which effects the security of Phantom (or any Phantom plugins created by the SAFEPublishing group), users of the effected application will be notified within 24 hours of discovery. 29 | 30 | The following chain of events will happen, in order: 31 | 32 | 1. We will diagnose the vulnerability within 24 hours and attempt to provide a fix. 33 | 2. If we are able to provide a fix, we will first update the `safe://phantom` application, then publicise the vulnerability on our SAFE Forum page, and the Phantom blog located at `safe://phantomblog` 34 | 3. If we are unable to provide a fix, we will publicize the vulnerability on our SAFE Forum page, and the Phantom blog located at `safe://phantomblog`, along with mitigation strategies and a timeline for the fix. -------------------------------------------------------------------------------- /deploy/README.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | 3 | This directory is geared for this projects own internal use, but can be co-opted for other uses if required. 4 | 5 | ### Dependencies 6 | 7 | _The versions listed here are the current versions we use internally, we don't provide any guarantee for deployment to work on any earlier on later versions._ 8 | 9 | * [safe_vault _(v0.20.1)_](https://github.com/maidsafe/safe_vault/releases/tag/0.20.1) 10 | * [safe_cli _(v0.8.1)_](https://github.com/maidsafe/safe-api/releases/tag/0.8.1) 11 | 12 | All dependencies must be in the system $PATH, such that the binaries are directly accessible, E.G. `shane@example:~/ safe ...` 13 | 14 | ### Deployment 15 | 16 | To deploy the project to a new public name, run the following command (in a bash environment), replacing `$PUBLIC_NAME` with your desired public name: 17 | 18 | `./deploy $PUBLIC_NAME --create` 19 | 20 | If you are deploying to an existing public name, use the following: 21 | 22 | `./deploy $PUBLIC_NAME` -------------------------------------------------------------------------------- /deploy/deploy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DEPLOYDIR="../www.phantom/dist/" 4 | HELPSTRING="--help" 5 | if [[ "$1" == "$HELPSTRING" ]] 6 | then 7 | echo "To deploy the project to a new public name, run the following command (in a bash environment), replacing \$PUBLIC_NAME with your desired public name:" 8 | echo "" 9 | echo " ./deploy \$PUBLIC_NAME --create" 10 | echo "" 11 | echo "If you are deploying to an existing public name, use the following:" 12 | echo "" 13 | echo " ./deploy \$PUBLIC_NAME"; 14 | exit 0 15 | fi 16 | 17 | if [ -z "$1" ] 18 | then 19 | echo "Error: a valid safenet NRS public name must be passed as the first parameter" 20 | exit 2 21 | fi 22 | 23 | (cd ../www.phantom; npm run build) 24 | 25 | if [[ ! -d "$DEPLOYDIR" ]] 26 | then 27 | echo "$DEPLOYDIR does not exist on your filesystem." 28 | fi 29 | 30 | CREATESTRING="--create" 31 | if [[ "$2" == "$CREATESTRING" ]] 32 | then 33 | PUTRESULT=`safe files put "$DEPLOYDIR" "safe://$1" --recursive` 34 | SAFEPATH="$(echo "$PUTRESULT" | grep -oE "at: \"safe:\/\/[a-zA-Z0-9]+" | grep -oE "safe:\/\/.*")" 35 | safe nrs create "$1" -l "$SAFEPATH?v=0" && echo "Deployment complete" || echo "Unable to create NRS, NRS may already exist, try re-running this without the --create argument" && exit 1 36 | else 37 | echo "$DEPLOYDIR" 38 | safe files sync "$DEPLOYDIR" "safe://$1" --update-nrs --recursive 39 | fi -------------------------------------------------------------------------------- /www.phantom/.browserlistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /www.phantom/README.md: -------------------------------------------------------------------------------- 1 | # Phantom 2 | 3 | ### Dependencies 4 | 5 | The following _global_ NPM dependencies are required: 6 | 7 | * @vue/cli@4.0.3 8 | 9 | _Dependencies may be installed using the following command:_ 10 | 11 | ``` 12 | npm install -g @vue/cli 13 | ``` 14 | 15 | ### Installation 16 | 17 | After installing the dependencies above, run the following command within this directory: 18 | 19 | ``` 20 | npm install 21 | ``` 22 | 23 | ### Development 24 | 25 | Run the following command within this directory: 26 | 27 | ``` 28 | npm run serve 29 | ``` 30 | 31 | Then in your browser proceed to `http://localhost:5000` and you will see a development-only mocked version of Phantom. 32 | 33 | ### Deployment 34 | 35 | Check the [Deploy](https://github.com/SAFEPublishing/Phantom/tree/master/deploy) directory for information about deploying. The deployment tool handles the NPM production builds. -------------------------------------------------------------------------------- /www.phantom/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ], 5 | plugins: [ 6 | "@babel/plugin-syntax-dynamic-import" 7 | ] 8 | }; 9 | -------------------------------------------------------------------------------- /www.phantom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phantom", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve --mode development", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "core-js": "^3.6.4", 11 | "highlight.js": "^9.18.1", 12 | "vue": "^2.6.11", 13 | "vue-router": "^3.1.6" 14 | }, 15 | "devDependencies": { 16 | "@vue/cli-plugin-babel": "^4.2.3", 17 | "@vue/cli-service": "^4.2.3", 18 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 19 | "node-sass": "^4.13.0", 20 | "sass-loader": "^8.0.2", 21 | "uglifyjs-webpack-plugin": "^2.2.0", 22 | "vue-loader": "^15.9.0", 23 | "vue-template-compiler": "^2.6.11" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /www.phantom/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /www.phantom/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAFEPublishing/Phantom/19252fbb39d35716b3d8992724d016f6128452ed/www.phantom/public/favicon.png -------------------------------------------------------------------------------- /www.phantom/public/font/opensans-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAFEPublishing/Phantom/19252fbb39d35716b3d8992724d016f6128452ed/www.phantom/public/font/opensans-bold.woff2 -------------------------------------------------------------------------------- /www.phantom/public/font/opensans-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAFEPublishing/Phantom/19252fbb39d35716b3d8992724d016f6128452ed/www.phantom/public/font/opensans-regular.woff2 -------------------------------------------------------------------------------- /www.phantom/public/img/card-temporary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAFEPublishing/Phantom/19252fbb39d35716b3d8992724d016f6128452ed/www.phantom/public/img/card-temporary.png -------------------------------------------------------------------------------- /www.phantom/public/img/posts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAFEPublishing/Phantom/19252fbb39d35716b3d8992724d016f6128452ed/www.phantom/public/img/posts.png -------------------------------------------------------------------------------- /www.phantom/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Phantom: Publishing tools for the SAFE network 8 | 41 | 42 | 43 | 46 |
47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /www.phantom/public/theme/banana/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAFEPublishing/Phantom/19252fbb39d35716b3d8992724d016f6128452ed/www.phantom/public/theme/banana/banner.png -------------------------------------------------------------------------------- /www.phantom/public/theme/banana/readme.md: -------------------------------------------------------------------------------- 1 | #### Banana 2 | 3 | This is a test theme created by the developers for testing the theme linting tool. This is not supposed to be used in production, but technically there is nothing stopping you from doing so. Have fun. -------------------------------------------------------------------------------- /www.phantom/public/theme/banana/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: yellow; 3 | } -------------------------------------------------------------------------------- /www.phantom/public/theme/banana/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "/theme/shared/theme.json", 3 | "name": "Banana", 4 | "description": "A theme the developers are using to test the theme import linter", 5 | "banner": "/theme/banana/banner.png", 6 | "styles": [ 7 | "/theme/banana/style.css" 8 | ] 9 | } -------------------------------------------------------------------------------- /www.phantom/public/theme/dark/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAFEPublishing/Phantom/19252fbb39d35716b3d8992724d016f6128452ed/www.phantom/public/theme/dark/banner.png -------------------------------------------------------------------------------- /www.phantom/public/theme/dark/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #2a3b5d; 3 | } 4 | 5 | .menu a { 6 | color: #fff; 7 | } 8 | 9 | .posts a { 10 | color: #fff; 11 | } 12 | 13 | .posts .title { 14 | color: #5392d4; 15 | } 16 | 17 | .post { 18 | color: #fff; 19 | } -------------------------------------------------------------------------------- /www.phantom/public/theme/dark/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "/theme/shared/theme.json", 3 | "name": "Dark", 4 | "description": "A simple, dark theme. For those of us who prefer the lights dimmed.", 5 | "banner": "/theme/dark/banner.png", 6 | "styles": [ 7 | "/theme/dark/style.css" 8 | ] 9 | } -------------------------------------------------------------------------------- /www.phantom/public/theme/light/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAFEPublishing/Phantom/19252fbb39d35716b3d8992724d016f6128452ed/www.phantom/public/theme/light/banner.png -------------------------------------------------------------------------------- /www.phantom/public/theme/light/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #fff; 3 | } 4 | 5 | .menu a { 6 | color: #000; 7 | } 8 | 9 | .posts a { 10 | color: #000; 11 | } 12 | 13 | .posts .title { 14 | color: #5392d4; 15 | } 16 | 17 | .post { 18 | color: #000; 19 | } -------------------------------------------------------------------------------- /www.phantom/public/theme/light/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "/theme/shared/theme.json", 3 | "name": "Light", 4 | "description": "A simple, white theme. Enough to get you started with a SAFE blog of your own!", 5 | "banner": "/theme/light/banner.png", 6 | "styles": [ 7 | "/theme/light/style.css" 8 | ] 9 | } -------------------------------------------------------------------------------- /www.phantom/public/theme/shared/default.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body, html { 6 | padding: 0; 7 | margin: 0; 8 | } 9 | 10 | body { 11 | font-family: sans-serif; 12 | } 13 | 14 | .root { 15 | width: 100%; 16 | padding: 40px; 17 | margin: 0 auto; 18 | max-width: 600px; 19 | } 20 | 21 | .menu .logo { 22 | text-transform: uppercase; 23 | font-weight: bold; 24 | font-size: 32px; 25 | } 26 | 27 | .menu a { 28 | display: inline-block; 29 | vertical-align: top; 30 | margin-top: 10px; 31 | text-decoration: none; 32 | font-weight: bold; 33 | opacity: .7; 34 | } 35 | 36 | .posts { 37 | padding-top: 40px; 38 | } 39 | 40 | .posts a { 41 | display: block; 42 | margin-bottom: 40px; 43 | text-decoration: none; 44 | } 45 | 46 | .posts .title { 47 | font-size: 24px; 48 | font-weight: bold; 49 | } 50 | 51 | .posts p { 52 | line-height: 22px; 53 | } 54 | 55 | .post { 56 | padding-top: 20px; 57 | } 58 | 59 | .posts img, .post img { 60 | max-width: 100%; 61 | } -------------------------------------------------------------------------------- /www.phantom/public/theme/shared/main.js: -------------------------------------------------------------------------------- 1 | Vue.use(VueRouter); 2 | 3 | window.routes = [{ 4 | path: "/", 5 | component: Vue.component("home", { template: "#postList" }) 6 | }]; 7 | 8 | var types = ['post', 'page']; 9 | for (var t = 0; t < types.length; t++) { 10 | var typeData = types[t] + 's'; 11 | 12 | for (var i = 0; i < window[typeData].length; i++) { 13 | window.routes.push({ 14 | path: window[typeData][i].path, 15 | component: Vue.component(window[typeData][i].path, { 16 | template: '
' + window[typeData][i].template + '
' 17 | }) 18 | }); 19 | } 20 | } 21 | 22 | var router = new VueRouter({ 23 | mode: 'hash', 24 | routes: window.routes 25 | }); 26 | 27 | window.menuItems = []; 28 | 29 | if (typeof window.themeConfig['menu-items'] !== "undefined") { 30 | for (i = 0; i < window.themeConfig["menu-items"].length; i++) { 31 | window.menuItems.push({ 32 | link: window.themeConfig['menu-items'][i].URL, 33 | text: window.themeConfig['menu-items'][i].Text 34 | }); 35 | } 36 | } 37 | 38 | Vue.component('Menu', { 39 | data() { 40 | return { 41 | menuItems: window.menuItems, 42 | logo: typeof window.themeConfig.logo !== "undefined" ? window.themeConfig.logo.Image.xorurl : false, 43 | blogName: window.blogName, 44 | themeConfig: window.themeConfig 45 | } 46 | }, 47 | template: '#menu' 48 | }); 49 | 50 | Vue.component('Content', { 51 | template: '#content', 52 | data() { 53 | return { 54 | themeConfig: window.themeConfig 55 | } 56 | } 57 | }); 58 | 59 | new Vue({ 60 | router, 61 | el: "#app", 62 | template: ` 63 |
64 | 65 | 66 |
67 | ` 68 | }); 69 | -------------------------------------------------------------------------------- /www.phantom/public/theme/shared/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 18 | 19 | 24 | 25 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /www.phantom/public/theme/shared/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "/theme/shared/template.html", 3 | "scripts": [ 4 | "/theme/shared/vue.js", 5 | "/theme/shared/main.js" 6 | ], 7 | "styles": [ 8 | "/theme/shared/default.css" 9 | ], 10 | "config": [ 11 | { 12 | "name": "logo", 13 | "description": "This logo will be displayed in the header of every page", 14 | "count": "single", 15 | "fields": [ 16 | { 17 | "name": "Image", 18 | "type": "file", 19 | "description": "Upload an image file (.png, .jpeg)" 20 | } 21 | ] 22 | }, 23 | { 24 | "name": "menu-items", 25 | "description": "Each URL you add will be displayed in the menu on every page", 26 | "count": "multi", 27 | "fields": [ 28 | { 29 | "name": "URL", 30 | "type": "text", 31 | "description": "A relative or absolute SAFE network URL" 32 | }, 33 | { 34 | "name": "Text", 35 | "type": "text", 36 | "description": "The text to display within the link" 37 | } 38 | ] 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /www.phantom/public/theme/zen/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAFEPublishing/Phantom/19252fbb39d35716b3d8992724d016f6128452ed/www.phantom/public/theme/zen/banner.png -------------------------------------------------------------------------------- /www.phantom/public/theme/zen/i18n/en_GB.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme_description": "A clean, minimalist theme which puts the focus on the blog content", 3 | "blog_header_description": "This will be displayed in the header of every page", 4 | "blog_header_name_description": "The name of your blog", 5 | "blog_header_description_description": "A short description for your blog", 6 | "menu_items_description": "Each URL you add will be displayed in the menu on every page", 7 | "menu_item_url_description": "A relative or absolute SAFE network URL", 8 | "menu_item_text_description": "The text to display within the URL" 9 | } -------------------------------------------------------------------------------- /www.phantom/public/theme/zen/i18n/es_ES.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme_description": "Un tema limpio y minimalista que pone el foco en el contenido del blog", 3 | "blog_header_description": "Esto se mostrará en el encabezado de cada página", 4 | "blog_header_name_description": "El nombre de tu blog", 5 | "blog_header_description_description": "Una breve descripción de tu blog", 6 | "menu_items_description": "Cada URL que agregue se mostrará en el menú de cada página", 7 | "menu_item_url_description": "Una URL de red SAFE relativa o absoluta", 8 | "menu_item_text_description": "El texto a mostrar dentro de la URL" 9 | } -------------------------------------------------------------------------------- /www.phantom/public/theme/zen/script.js: -------------------------------------------------------------------------------- 1 | Vue.filter('timeAgo', function(value) { 2 | let minute = 60, 3 | hour = 3600, 4 | day = 86400, 5 | month = 2592000, 6 | year = 31536000, 7 | elapsed = Math.floor((Date.now() - new Date(value)) / 1000); 8 | 9 | if (elapsed < minute) { 10 | return 'just now'; 11 | } 12 | 13 | let a = elapsed < hour && [Math.floor(elapsed / minute), 'minute'] || 14 | elapsed < day && [Math.floor(elapsed / hour), 'hour'] || 15 | elapsed < month && [Math.floor(elapsed / day), 'day'] || 16 | elapsed < year && [Math.floor(elapsed / month), 'month'] || 17 | [Math.floor(elapsed / year), 'year']; 18 | 19 | return a[0] + ' ' + a[1] + (a[0] === 1 ? '' : 's') + " ago"; 20 | }); -------------------------------------------------------------------------------- /www.phantom/public/theme/zen/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | background-color: #fff; 4 | font-family: sans-serif; 5 | } 6 | 7 | .root { 8 | width: 100%; 9 | padding: 20px; 10 | max-width: 700px; 11 | margin: 0 auto; 12 | } 13 | 14 | .menu { 15 | padding-bottom: 50px; 16 | background-color: #fff; 17 | white-space: nowrap; 18 | } 19 | 20 | .menu h1 { 21 | margin: 0; 22 | } 23 | 24 | .menu p { 25 | margin: 10px 0 0 0; 26 | } 27 | 28 | .menu a { 29 | display: inline-block; 30 | margin: 10px 10px 0 0; 31 | color: #000; 32 | font-weight: bold; 33 | } 34 | 35 | .posts a { 36 | display: block; 37 | margin-bottom: 50px; 38 | color: #000; 39 | text-decoration: none; 40 | } 41 | 42 | .posts a:hover h1 { 43 | text-decoration: underline; 44 | } 45 | 46 | .posts .post-date { 47 | font-size: 1.1em; 48 | opacity: .7; 49 | } 50 | 51 | .posts h1 { 52 | margin-top: 0; 53 | font-size: 1.5em; 54 | } 55 | 56 | .posts p { 57 | line-height: 20px; 58 | } 59 | 60 | .post { 61 | color: #000; 62 | } 63 | 64 | .post p, .post li, .post blockquote { 65 | line-height: 20px; 66 | } -------------------------------------------------------------------------------- /www.phantom/public/theme/zen/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 21 | 22 | 27 | 28 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /www.phantom/public/theme/zen/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Zen", 3 | "description": "theme_description", 4 | "banner": "/theme/zen/banner.png", 5 | "template": "/theme/zen/template.html", 6 | "scripts": [ 7 | "/theme/shared/vue.js", 8 | "/theme/zen/script.js", 9 | "/theme/shared/main.js" 10 | ], 11 | "styles": [ 12 | "/theme/zen/style.css" 13 | ], 14 | "locales": { 15 | "en_GB": "/theme/zen/i18n/en_GB.json", 16 | "es_ES": "/theme/zen/i18n/es_ES.json" 17 | }, 18 | "config": [ 19 | { 20 | "name": "blog-header", 21 | "description": "blog_header_description", 22 | "count": "single", 23 | "fields": [ 24 | { 25 | "name": "name", 26 | "type": "text", 27 | "description": "blog_header_name_description" 28 | }, 29 | { 30 | "name": "description", 31 | "type": "text", 32 | "description": "blog_header_description_description" 33 | } 34 | ] 35 | }, 36 | { 37 | "name": "menu-items", 38 | "description": "menu_items_description", 39 | "count": "multi", 40 | "fields": [ 41 | { 42 | "name": "URL", 43 | "type": "text", 44 | "description": "menu_item_url_description" 45 | }, 46 | { 47 | "name": "Text", 48 | "type": "text", 49 | "description": "menu_item_text_description" 50 | } 51 | ] 52 | } 53 | ] 54 | } -------------------------------------------------------------------------------- /www.phantom/src/App.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 33 | 34 | 209 | -------------------------------------------------------------------------------- /www.phantom/src/component/DocumentEdit.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 97 | 98 | -------------------------------------------------------------------------------- /www.phantom/src/component/DocumentList.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 126 | 127 | -------------------------------------------------------------------------------- /www.phantom/src/component/Foot.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 29 | 30 | -------------------------------------------------------------------------------- /www.phantom/src/component/Hamburger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | -------------------------------------------------------------------------------- /www.phantom/src/component/HomeMenu.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 61 | 62 | -------------------------------------------------------------------------------- /www.phantom/src/component/ImageGallery.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 90 | 91 | -------------------------------------------------------------------------------- /www.phantom/src/component/LanguageSelector.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 31 | 32 | -------------------------------------------------------------------------------- /www.phantom/src/component/Loader.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | 17 | -------------------------------------------------------------------------------- /www.phantom/src/component/Logo.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /www.phantom/src/component/Modal.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 26 | 27 | -------------------------------------------------------------------------------- /www.phantom/src/component/PageTitleWithActions.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 22 | -------------------------------------------------------------------------------- /www.phantom/src/component/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 65 | 66 | -------------------------------------------------------------------------------- /www.phantom/src/i18n/en_GB.js: -------------------------------------------------------------------------------- 1 | const en_GB = { 2 | home: "Home", 3 | about: "About", 4 | login: "Login", 5 | home_heading: "Phantom makes it easy to publish", 6 | home_subheading: "Take your creativity to the next level without compromising your anonymity", 7 | home_cta: "Get started - it's free", 8 | home_builtwith: "Built with Phantom", 9 | home_builtwith_subheading: "Our top picks from the Phantom ecosystem", 10 | home_builtwith_cta: "Check it out", 11 | footer_about: "This project was lovingly created by Shane Armstrong for the SAFE Network community.", 12 | footer_links: "Warning: The following links are on the clearnet:", 13 | footer_github: "Phantom Github repository", 14 | footer_forum: "Phantom SAFE forum page", 15 | footer_liability: "This project will always remain on the SAFE network address safe://phantom, Phantom delivered from any other SAFE network address is not endorsed or supported by us in any way.", 16 | sidebar_editing: "Currently Editing:", 17 | manage: "Manage", 18 | write: "Write", 19 | dashboard: "Dashboard", 20 | domains: "Domains", 21 | themes: "Themes", 22 | theme_config: "Theme Config", 23 | posts: "Posts", 24 | pages: "Pages", 25 | logout: "Logout", 26 | domain_name: "Domain Name", 27 | files_container: "Files Container", 28 | updated: "Updated", 29 | created: "Created", 30 | second_ago: "[TIME] second ago", 31 | seconds_ago: "[TIME] seconds ago", 32 | minutes_ago: "[TIME] minutes ago", 33 | minute_ago: "[TIME] minute ago", 34 | hours_ago: "[TIME] hours ago", 35 | hour_ago: "[TIME] hour ago", 36 | days_ago: "[TIME] days ago", 37 | day_ago: "[TIME] day ago", 38 | weeks_ago: "[TIME] weeks ago", 39 | week_ago: "[TIME] week ago", 40 | months_ago: "[TIME] months ago", 41 | month_ago: "[TIME] month ago", 42 | years_ago: "[TIME] years ago", 43 | year_ago: "[TIME] year ago", 44 | just_now: "Just now", 45 | use: "Use", 46 | create_domain: "Create domain", 47 | public_name: "Public Name", 48 | public_name_label: "This will be the URL people use to load your website. For example, if you entered \"funkyduck\", users would access it via safe://funkyduck", 49 | theme_installed: "This theme is currently installed", 50 | theme_check: "Would you like to check for updates?", 51 | install: "Install", 52 | save: "Save", 53 | add: "Add", 54 | theme_import: "Import theme", 55 | theme_url: "Theme URL", 56 | theme_url_label: "The SAFE network URL of the theme's manifest, this URL will end in \".json\"", 57 | cancel: "Cancel", 58 | select_domain: "Please select a domain from the list above to begin editing your website", 59 | no_domains: "You currently have no domains registered to this machine, please click \"create domain\" to get started", 60 | no_posts: "You currently have no Posts, please click \"create post\" to get started", 61 | no_pages: "You currently have no Pages, please click \"Create page\" to get started", 62 | title: "Title", 63 | status: "Status", 64 | create_post: "Create post", 65 | create_page: "Create page", 66 | edit: "Edit", 67 | post_default_1: "Phantom uses **Markdown** to edit Posts.", 68 | post_default_2: "It's _really_ easy to get started, just click anywhere and ~start~ begin typing. The content will automatically update as you add new Markdown tags.", 69 | post_default_3: "This is pretty cool, right?", 70 | post_default_4: "We thought so too!", 71 | post_default_5: "When you're done editing, click on the save button at the top of the page.", 72 | draft_post: "Draft post", 73 | save_post: "Save post", 74 | save_page: "Save page", 75 | publish_drafts: "Publish drafts", 76 | unpublished_drafts: "Unpublished drafts", 77 | unpublished_drafts_help: "To release a new version of your blog including all unpublished drafts click \"Publish drafts\" at the top right.", 78 | image_gallery: "Image Gallery", 79 | image_gallery_help: "The input beneath each image contains Markdown you can copy and paste in to your post to display the image.", 80 | image: "Image", 81 | description: "Description", 82 | description_label: "A description for this image", 83 | upload: "Upload", 84 | event: "Event", 85 | event_log: "Event log", 86 | information: "Information", 87 | no_events: "There have been no events", 88 | delete: "Delete" 89 | }; 90 | 91 | export default en_GB; -------------------------------------------------------------------------------- /www.phantom/src/i18n/es_ES.js: -------------------------------------------------------------------------------- 1 | const es_ES = { 2 | home: "Inicio", 3 | about: "Acerca de", 4 | login: "Inicio de sesión", 5 | home_heading: "Phantom hace que sea fácil publicar", 6 | home_subheading: "Lleva tu creatividad al siguiente nivel sin comprometer tu anonimato.", 7 | home_cta: "Empieza, es gratis.", 8 | home_builtwith: "Construido con Phantom", 9 | home_builtwith_subheading: "Nuestras mejores selecciones del ecosistema Phantom", 10 | home_builtwith_cta: "Compruébalo", 11 | footer_about: "Este proyecto fue creado con cariño por Shane Armstrong para la comunidad de la Red SAFE.", 12 | footer_links: "Advertencia: Los siguientes enlaces están en el clearnet:", 13 | footer_github: "Repositorio Phantom en Github", 14 | footer_forum: "Página de Phantom en el foro SAFE", 15 | footer_liability: "Este proyecto siempre permanecerá en la dirección de la red SAFE safe://phantom, cualquier otra dirección de la red SAFE no está avalado ni apoyado por nosotros de ninguna manera.", 16 | sidebar_editing: "Actualmente en edición:", 17 | manage: "Administrar", 18 | write: "Editar", 19 | dashboard: "Dashboard", 20 | domains: "Dominios", 21 | themes: "Temas", 22 | theme_config: "Configuración del tema", 23 | posts: "Posts", 24 | pages: "Páginas", 25 | logout: "Cierre de sesión", 26 | domain_name: "El nombre de dominio", 27 | files_container: "Contenedor de archivos", 28 | updated: "Actualizado", 29 | created: "Creado", 30 | seconds_ago: "Hace [TIME] segundos", 31 | second_ago: "Hace [TIME] segundo", 32 | minutes_ago: "Hace [TIME] minutos", 33 | minute_ago: "Hace [TIME] minuto", 34 | hours_ago: "Hace [TIME] horas", 35 | hour_ago: "[TIME] hora antes", 36 | days_ago: "Hace [TIME] días", 37 | day_ago: "Hace [TIME] día", 38 | weeks_ago: "Hace [TIME] semanas", 39 | week_ago: "Hace [TIME] semana", 40 | months_ago: "Hace [TIME] meses", 41 | month_ago: "Hace [TIME] mes", 42 | years_ago: "Hace [TIME] años", 43 | year_ago: "Hace [TIME] año", 44 | just_now: "Ahora", 45 | use: "Usar", 46 | create_domain: "Crear el dominio", 47 | public_name: "Nombre público", 48 | public_name_label: "Esta será la URL que la gente use para cargar su sitio web. Por ejemplo, si se introduce \"funkyduck\", los usuarios accederán a ella a través de safe://funkyduck", 49 | theme_installed: "Este tema está actualmente instalado", 50 | theme_check: "¿Le gustaría comprobar las actualizaciones?", 51 | install: "Instalar", 52 | save: "Guardar", 53 | add: "Añadir", 54 | theme_import: "Importar tema", 55 | theme_url: "URL del tema", 56 | theme_url_label: "La URL del manifiesto del tema en la red SAFE, este URL terminará en \".json\"", 57 | cancel: "Cancelar", 58 | select_domain: "Seleccione un dominio de la lista anterior.", 59 | no_domains: "Actualmente no tienes dominios, por favor haz clic en \"Crear dominio\" para empezar.", 60 | no_posts: "Actualmente no tienes posts, por favor haz clic en \"Crear post\" para empezar.", 61 | no_pages: "Actualmente no tienes páginas, por favor haz clic en \"Crear página\" para empezar.", 62 | title: "Título", 63 | status: "Estado", 64 | create_post: "Crear post", 65 | create_page: "Crear página", 66 | edit: "Editar", 67 | post_default_1: "Phantom usa **Markdown** para editar los mensajes.", 68 | post_default_2: "Es _realmente_ fácil de empezar, sólo tienes que hacer clic en cualquier lugar y ~empezar~ a escribir. El contenido se actualizará automáticamente a medida que añadas nuevas etiquetas de Markdown.", 69 | post_default_3: "Esto es bastante guay, ¿verdad?", 70 | post_default_4: "¡Nosotros también lo pensamos!", 71 | post_default_5: "Cuando termines de editar, haz clic en el botón de guardar en la parte superior de la página.", 72 | draft_post: "Borrador del post", 73 | save_post: "Guardar post", 74 | save_page: "Guardar página", 75 | publish_drafts: "Publicar borradores", 76 | unpublished_drafts: "Borradores inéditos", 77 | unpublished_drafts_help: "Para implementar una nueva versión de tu blog que incluye todos los borradores no publicados, haga clic en \"Publicar borradores\" en la esquina superior derecha.", 78 | image_gallery: "Galería de imágenes", 79 | image_gallery_help: "La entrada debajo de cada imagen contiene un Markdown que puedes copiar y pegar en tu entrada para mostrar la imagen.", 80 | image: "Imagen", 81 | description: "Descripción", 82 | description_label: "Una descripción de esta imagen", 83 | upload: "Subir", 84 | event: "Evento", 85 | event_log: "Registro de eventos", 86 | information: "Información", 87 | no_events: "No se han producido eventos", 88 | delete: "Borrar" 89 | }; 90 | 91 | export default es_ES; -------------------------------------------------------------------------------- /www.phantom/src/i18n/nl_NL.js: -------------------------------------------------------------------------------- 1 | const es_ES = { 2 | home: "Home", 3 | about: "Over", 4 | login: "Log in", 5 | home_heading: "Phantom maakt publiceren makkelijk", 6 | home_subheading: "Breng je creativiteit naar het volgende level, zonder anonimiteit te verliezen", 7 | home_cta: "Start nu - het is gratis", 8 | home_builtwith: "Gemaakt met Phantom", 9 | home_builtwith_subheading: "Onze favorieten uit het Phantom ecosysteem", 10 | home_builtwith_cta: "Bekijk nu", 11 | footer_about: "Dit project is ontwikkeld door Shane Armstrong voor de SAFE Network community.", 12 | footer_links: "Waarschuwing: De volgende links openen op het internet:", 13 | footer_github: "Phantom Github repository", 14 | footer_forum: "Phantom SAFE forum pagina", 15 | footer_liability: "Dit project zal altijd te vinden zijn op SAFE network adress safe://phantom, Phantom verkregen van elke andere locatie wordt niet ondersteunt.", 16 | sidebar_editing: "Momenteel in bewerking:", 17 | manage: "Beheer", 18 | write: "Schrijven", 19 | dashboard: "Dashboard", 20 | domains: "Domeinen", 21 | themes: "Themas", 22 | theme_config: "Thema configuratie", 23 | posts: "Posts", 24 | pages: "Pagina's", 25 | logout: "Log uit", 26 | domain_name: "Domein naam", 27 | files_container: "Bestandsmap", 28 | updated: "Laatst bijgewerkt", 29 | created: "Gemaakt", 30 | seconds_ago: "[TIME] seconden geleden", 31 | second_ago: "[TIME] seconde geleden", 32 | minutes_ago: "[TIME] minuten geleden", 33 | minute_ago: "[TIME] minuut geleden", 34 | hours_ago: "[TIME] uur geleden", 35 | hour_ago: "[TIME] uur geleden", 36 | days_ago: "[TIME] dagen geleden", 37 | day_ago: "[TIME] dag geleden", 38 | weeks_ago: "[TIME] weken geleden", 39 | week_ago: "[TIME] week geleden", 40 | months_ago: "[TIME] maanden geleden", 41 | month_ago: "[TIME] maand geleden", 42 | years_ago: "[TIME] jaar geleden", 43 | year_ago: "[TIME] jaar geleden", 44 | just_now: "Net nu", 45 | use: "Gebruik", 46 | create_domain: "Creëer domein", 47 | public_name: "Publieke naam", 48 | public_name_label: "Dit wordt de URL waarmee mensen je website bereiken. Als mensen bijvoorbeeld \"funkyduck\" invullen, krijgen ze toegang tot safe://funkyduck", 49 | theme_installed: "Dit thema is momenteel geinstalleerd", 50 | theme_check: "Wil je checken of er updates zijn?", 51 | install: "Installeer", 52 | save: "Save", 53 | add: "Toevoegen", 54 | theme_import: "Importeer thema", 55 | theme_url: "Thema URL", 56 | theme_url_label: "De SAFE network URL van het thema manifest, deze url zal eindigen op \".json\"", 57 | cancel: "Cancel", 58 | select_domain: "Selecteer een domein uit de bovenstaande lijst", 59 | no_domains: "Je hebt momenteel geen domeinen, klik op 'creër domein' om te beginnen", 60 | no_posts: "Je hebt momenteel geen Posts, klik op 'creër post' om te beginnen", 61 | no_pages: "Je hebt momenteel geen Pagina's, klik op 'creër post' om te beginnen", 62 | title: "Titel", 63 | status: "Status", 64 | create_post: "Creër post", 65 | create_page: "Creër pagina", 66 | edit: "Bewerk", 67 | post_default_1: "Phantom gebruikt **Markdown**om posts aan te passen.", 68 | post_default_2: "Het_is_erg_makkelijk om te beginnen, click gewoon ergens en ~begin~ met typen. je text wordt automatisch veranderd als je markdown tags gebruikt.", 69 | post_default_3: "Best cool toch?", 70 | post_default_4: "Dat dachten wij ook!", 71 | post_default_5: "Als je klaar bent met aanpassen, klik dan op de save button bovenaan de pagina.", 72 | draft_post: "Concept post", 73 | save_post: "Save post", 74 | save_page: "Save pagina", 75 | publish_drafts: "Concepten publiceren", 76 | unpublished_drafts: "niet-gepubliceerde concepten", 77 | unpublished_drafts_help: "Om een nieuwe versie van jouw blog inclusief alle niet-gepubliceerde concepten uit te brengen, klikt u rechtsboven op \"Concepten publiceren\".", 78 | image_gallery: "Foto Galerij", 79 | image_gallery_help: "De inputbalk onder iedere afbeelding heeft Markdown die je kunt kopiëren en in je post kunt plakken om de afbeelding weer te geven.", 80 | image: "Afbeelding", 81 | description: "Omschrijving", 82 | description_label: "Een omschrijving voor deze afbeelding", 83 | upload: "Upload" 84 | }; 85 | 86 | export default es_ES; -------------------------------------------------------------------------------- /www.phantom/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import api from '@/service/safe/api'; 5 | import importer from '@/service/theme/importer'; 6 | import phantomPluginTools from '@/service/plugin/phantom'; 7 | import en_GB from '@/i18n/en_GB'; 8 | import es_ES from '@/i18n/es_ES'; 9 | import nl_NL from '@/i18n/nl_NL'; 10 | 11 | window.phantom = phantomPluginTools; 12 | 13 | let locales = { 14 | "en_GB": en_GB, 15 | "es_ES": es_ES, 16 | "nl_NL": nl_NL 17 | }; 18 | 19 | let localeNames = { 20 | "en_GB": "English (UK)", 21 | "es_ES": "Español (ES)", 22 | "nl_NL": "Nederlands (NL)" 23 | }; 24 | 25 | const data = { 26 | initialized: false, 27 | authenticated: false, 28 | domain: false, 29 | themeHasConfig: false, 30 | locale: "en_GB", 31 | localeNames: localeNames, 32 | localeTranslations: locales 33 | }; 34 | 35 | // This is the only place we don't use the async safe libs, because without this data initial routing (with guards) is impossible 36 | // So we hack in this data... then we verify 37 | let tempAuth = localStorage.getItem("auth"); 38 | let tempNRS = localStorage.getItem("current-nrs"); 39 | let tempLocale = localStorage.getItem("locale"); 40 | data.authenticated = tempAuth ? !!JSON.parse(tempAuth).data : false; 41 | data.domain = tempNRS ? JSON.parse(tempNRS).data : false; 42 | data.locale = tempLocale ? tempLocale : data.locale; 43 | 44 | /** 45 | * This function handles translating and outputting of text 46 | * If there is a locale code entry for the current text code it will output it, else it will default to English 47 | */ 48 | function translate(value, fallback) { 49 | if (locales.hasOwnProperty(data.locale) && typeof locales[data.locale][value] !== "undefined") { 50 | return locales[data.locale][value]; 51 | } 52 | 53 | return typeof locales.en_GB[value] !== "undefined" 54 | ? locales.en_GB[value] 55 | : (typeof(fallback) !== "undefined" 56 | ? fallback 57 | : value); 58 | } 59 | Vue.filter('t', translate); 60 | document.title = translate("home_heading"); 61 | 62 | router.beforeEach((to, from, next) => { 63 | // Should we show 64 | if (to.meta.hasOwnProperty('authenticated') && data.authenticated !== to.meta.authenticated) { 65 | return next(data.authenticated ? '/app' : '/'); 66 | } 67 | 68 | if (to.meta.hasOwnProperty('domain') && to.meta.domain === true && data.domain === false) { 69 | return next('/app/domains'); 70 | } 71 | 72 | next(); 73 | }); 74 | 75 | router.beforeResolve((to, from, next) => { 76 | if (data.authenticated) { 77 | // Preload our themes 78 | api.getInstalledThemes().then(async function(themes) { 79 | if (!themes.length) { 80 | let urls = ["/theme/zen/theme.json", "/theme/light/theme.json", "/theme/dark/theme.json"]; 81 | 82 | for (let i = 0; i < urls.length; i++) { 83 | await importer.import(urls[i], data.localeTranslations, translate); 84 | } 85 | } 86 | 87 | if (data.domain) { 88 | api.getTheme(data.domain).then(theme => { 89 | data.themeHasConfig = 90 | typeof theme.config.config !== "undefined" 91 | && Array.isArray(theme.config.config) 92 | && theme.config.config.length; 93 | 94 | if (data.themeHasConfig && typeof theme.config.localeTranslations !== "undefined") { 95 | for (let locale in theme.config.localeTranslations) { 96 | if (theme.config.localeTranslations.hasOwnProperty(locale)) { 97 | if (typeof locales[locale] === "undefined") { 98 | locales[locale] = {}; 99 | } 100 | 101 | for (let key in theme.config.localeTranslations[locale]) { 102 | if (theme.config.localeTranslations[locale].hasOwnProperty(key)) { 103 | if (typeof locales[locale][key] === "undefined") { 104 | locales[locale][key] = theme.config.localeTranslations[locale][key]; 105 | } 106 | } 107 | } 108 | } 109 | } 110 | } 111 | }); 112 | } 113 | }); 114 | } 115 | 116 | next(); 117 | }); 118 | 119 | Vue.filter('safeURL', function (value) { 120 | return value.startsWith('safe://') ? value : "safe://" + value; 121 | }); 122 | 123 | Vue.filter('idToReadableString', function (value) { 124 | return value.replace(/[\-_]+/, " "); 125 | }); 126 | 127 | Vue.filter('timeAgo', function(value) { 128 | let minute = 60, 129 | hour = 3600, 130 | day = 86400, 131 | month = 2592000, 132 | year = 31536000, 133 | elapsed = Math.floor((Date.now() - new Date(value)) / 1000); 134 | 135 | if (elapsed < minute) { 136 | return translate('just_now'); 137 | } 138 | 139 | let a = elapsed < hour && [Math.floor(elapsed / minute), 'minute'] || 140 | elapsed < day && [Math.floor(elapsed / hour), 'hour'] || 141 | elapsed < month && [Math.floor(elapsed / day), 'day'] || 142 | elapsed < year && [Math.floor(elapsed / month), 'month'] || 143 | [Math.floor(elapsed / year), 'year']; 144 | 145 | let text = translate(a[1] + (a[0] === 1 ? '' : 's') + "_ago"); 146 | return text.replace("[TIME]", a[0]); 147 | }); 148 | 149 | ArrayBuffer.prototype.toString = function() { 150 | let decoder = new TextDecoder(); 151 | return decoder.decode(this); 152 | }; 153 | 154 | String.prototype.toBuffer = function() { 155 | let encoder = new TextEncoder(); 156 | return encoder.encode(this.valueOf()); 157 | }; 158 | 159 | Vue.config.productionTip = false; 160 | 161 | new Vue({ 162 | router, 163 | data: data, 164 | render: h => h(App), 165 | created: function() { 166 | api.getAuthToken().then(token => { 167 | return !token ? false : api.authenticate(token).then(response => { 168 | this.$root.$data.authenticated = true; 169 | 170 | api.getCurrentDomain().then(domain => { 171 | this.$root.$data.domain = domain; 172 | }); 173 | }); 174 | }); 175 | } 176 | }).$mount('#app') 177 | -------------------------------------------------------------------------------- /www.phantom/src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Home from './view/Home.vue' 4 | import About from './view/About.vue' 5 | import Dashboard from './view/Dashboard.vue' 6 | import Domains from './view/Domains.vue' 7 | import DomainCreate from './view/DomainCreate.vue' 8 | import Posts from './view/Posts.vue' 9 | import PostEdit from './view/PostEdit.vue' 10 | import Pages from './view/Pages.vue' 11 | import PageEdit from './view/PageEdit.vue' 12 | import Themes from './view/Themes.vue' 13 | import Theme from './view/Theme.vue' 14 | import DevLog from './view/Dev/Log.vue'; 15 | import DocsWrapper from './view/Docs/Wrapper.vue'; 16 | 17 | Vue.use(Router); 18 | 19 | let loggedIn = { authenticated: true }, 20 | loggedOut = { authenticated: false }, 21 | hasDomain = { authenticated: true, domain: true }; 22 | 23 | export default new Router({ 24 | mode: 'hash', 25 | base: process.env.BASE_URL, 26 | routes: [ 27 | { 28 | path: '/', 29 | component: Home, 30 | meta: loggedOut 31 | }, 32 | { 33 | path: '/about', 34 | component: About, 35 | meta: loggedOut 36 | }, 37 | { 38 | path: '/app/domains', 39 | component: Domains, 40 | meta: loggedIn 41 | }, 42 | { 43 | path: '/app/domains/create', 44 | component: DomainCreate, 45 | meta: loggedIn 46 | }, 47 | { 48 | path: '/app', 49 | component: Dashboard, 50 | meta: hasDomain, 51 | }, 52 | { 53 | path: '/app/themes', 54 | component: Themes, 55 | meta: hasDomain 56 | }, 57 | { 58 | path: '/app/theme', 59 | component: Theme, 60 | meta: hasDomain 61 | }, 62 | { 63 | path: '/app/posts', 64 | component: Posts, 65 | meta: hasDomain, 66 | }, 67 | { 68 | path: '/app/post/:file', 69 | component: PostEdit, 70 | meta: hasDomain, 71 | }, 72 | { 73 | path: '/app/pages', 74 | component: Pages, 75 | meta: hasDomain, 76 | }, 77 | { 78 | path: '/app/page/:file', 79 | component: PageEdit, 80 | meta: hasDomain, 81 | }, 82 | { 83 | path: '/app/log', 84 | component: DevLog, 85 | meta: loggedIn, 86 | }, 87 | { 88 | path: '/docs', 89 | component: DocsWrapper, 90 | meta: loggedOut, 91 | children: [ 92 | { 93 | path: '', 94 | component: () => import('@/view/Docs/Home.vue') 95 | } 96 | ] 97 | } 98 | ], 99 | scrollBehavior (to, from, savedPosition) { 100 | return { x: 0, y: 0 } 101 | } 102 | }) 103 | -------------------------------------------------------------------------------- /www.phantom/src/service/log/logger.js: -------------------------------------------------------------------------------- 1 | const logger = { 2 | createEvent: function(name, info) { 3 | return new Promise((resolve, reject) => { 4 | let event = { 5 | name: name, 6 | info: info, 7 | created: (new Date).toISOString() 8 | }; 9 | 10 | let events = localStorage.getItem('events'); 11 | events = events ? JSON.parse(events) : []; 12 | events.unshift(event); 13 | events = events.slice(0, 250); 14 | 15 | localStorage.setItem('events', JSON.stringify(events)); 16 | resolve(events); 17 | }); 18 | }, 19 | getEvents: function() { 20 | return new Promise((resolve, reject) => resolve(JSON.parse(localStorage.getItem('events')))); 21 | }, 22 | clear: function() { 23 | let clearEvent = [{ 24 | name: 'log', 25 | info: 'Event log cleared', 26 | created: (new Date).toISOString() 27 | }]; 28 | 29 | localStorage.setItem('events', JSON.stringify(clearEvent)) 30 | 31 | return new Promise((resolve, reject) => resolve(clearEvent)); 32 | } 33 | }; 34 | 35 | export default logger; -------------------------------------------------------------------------------- /www.phantom/src/service/markdown/canonical.js: -------------------------------------------------------------------------------- 1 | import formatter from "./formatter"; 2 | 3 | const canonical = { 4 | getHTMLWrappedMarkdown: function (markdown) { 5 | let title = "/#/post/" + formatter.getSanitizedURI(formatter.getTitle(markdown)); 6 | return '' + markdown + ''; 7 | }, 8 | 9 | getMarkdownFromHTML: function(html) { 10 | let matches = html.match(/([\s\S]*)<\/body>/); 11 | return matches ? matches[1] : false; 12 | } 13 | }; 14 | 15 | export default canonical; -------------------------------------------------------------------------------- /www.phantom/src/service/markdown/formatter.js: -------------------------------------------------------------------------------- 1 | String.prototype.removeFirstTitle = function() { 2 | return this.replace(/^(#.*\n)/m, ""); 3 | }; 4 | 5 | String.prototype.replaceSingleTag = function(match, tag) { 6 | return this.replace(new RegExp(match + "(.*)" + match, "gm"), "<" + tag + ">" + "$1" + ""); 7 | }; 8 | 9 | String.prototype.replaceTagToEndOfLine = function(match, tag) { 10 | return this.replace(new RegExp("^" + match + "(.*)", "gm"), "<" + tag + ">" + "$1" + "\n"); 11 | }; 12 | 13 | String.prototype.replaceImages = function() { 14 | return this.replace(/\!\[([^\]]+)\]\(([^)]+)\)/g, '$1'); 15 | }; 16 | 17 | String.prototype.replaceAnchors = function() { 18 | return this.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); 19 | }; 20 | 21 | String.prototype.replaceBlockQuotes = function() { 22 | return this.replace(/^(>|>)[ \t]?(.+)/gm, '
$2
'); 23 | }; 24 | 25 | String.prototype.replaceCodeInline = function() { 26 | return this.replace(/`(.+)`/gm, '$1'); 27 | }; 28 | 29 | String.prototype.replaceCodeSpans = function() { 30 | return this.replace(/^```([a-zA-Z0-9]*)\n([\s\S]*?)```/gm, function(match, lang, code) { 31 | return '
' + code.replace(/\n/g, "{CODEBLOCK_NEWLINE}") + '
'; 32 | }); 33 | }; 34 | 35 | String.prototype.replaceCodeSpanNewLines = function() { 36 | return this.replace(/\{CODEBLOCK_NEWLINE\}/gm, "\n"); 37 | }; 38 | 39 | String.prototype.replaceHorizontalRules = function() { 40 | return this.replace(/^(\_{3,}|\-{3,}|\*{3,})$/gm, "
"); 41 | }; 42 | 43 | String.prototype.getSanitizedMarkdown = function() { 44 | return this 45 | // Replace multiple newlines with a single new line 46 | .replace(/(
){1,}/g, "\n") 47 | // Sometimes content editable adds empty divs, clean them up here, we should have 0 divs 48 | .replace(/(
)+/g, "\n") 49 | .replace(/(<\/div>)+/g, "") 50 | }; 51 | 52 | const formatter = { 53 | getDefaultMarkdown: function(type, translate) { 54 | let text = "#" + translate("draft_post"); 55 | 56 | for (let i = 1; i <= 5; i++) { 57 | text += "\n" + translate("post_default_" + i); 58 | } 59 | 60 | return text; 61 | }, 62 | 63 | getTitle: function(markdown) { 64 | // Index 1 because split creates the first index as an empty string 65 | return markdown.split(/#(.+)/)[1]; 66 | }, 67 | 68 | // Extracts the first 35 words from the post content, not including the title 69 | getExcerpt: function(markdown) { 70 | return markdown 71 | .removeFirstTitle() 72 | .replace(/[^a-zA-Z0-9 ,.&\-_:;@~()!\"£$%^=+{}\[\]\/\\*']+/gm, "") 73 | .split(" ") 74 | .slice(0, 35) 75 | .join(" ") + "…" 76 | }, 77 | 78 | /** 79 | * Takes markdown as an input and spits out formatted HTML 80 | * 81 | * @param markdown string 82 | * @param keepTitle bool 83 | * @returns string 84 | */ 85 | getParsedHTML: function(markdown, keepTitle) { 86 | markdown = keepTitle === true ? markdown : markdown.removeFirstTitle(); 87 | 88 | return markdown 89 | .replaceHorizontalRules() 90 | .replaceSingleTag("\\*\\*", "b") 91 | .replaceSingleTag("\\_", "i") 92 | .replaceSingleTag("\\~", "s") 93 | .replaceTagToEndOfLine("#####", "h5") 94 | .replaceTagToEndOfLine("####", "h4") 95 | .replaceTagToEndOfLine("###", "h3") 96 | .replaceTagToEndOfLine("##", "h2") 97 | .replaceTagToEndOfLine("#", "h1") 98 | // Now that the bold tags are dealt with, we can format lists safely 99 | .replaceTagToEndOfLine("\\*", "li") 100 | // Images must be injected before anchors due to the similar syntax 101 | .replaceImages() 102 | .replaceAnchors() 103 | .replaceCodeSpans() 104 | .replaceCodeInline() 105 | .replaceBlockQuotes() 106 | // Now we've dealt with every other special case we can insert paragraphs to the left overs 107 | .replace(/^((?!$1

") 108 | // Purge all the remaining new lines so we can inject logical new lines 109 | .replace(/\n/g, "") 110 | // Re-inject newlines in code spans 111 | .replaceCodeSpanNewLines() 112 | // Now we can add new lines where they _should_ be 113 | .replace(/<\/(p|li|h[1-5])>/gm, "\n"); 114 | }, 115 | 116 | getEditableMarkdown: function(markdown) { 117 | return markdown.removeFirstTitle() 118 | .replace(/\n/g, "

"); 119 | }, 120 | 121 | getSanitizedURI: function(title) { 122 | return title.replace(/[^a-zA-Z0-9 \-\_]/g, "").toLocaleLowerCase().replace(/ /g, "-"); 123 | } 124 | }; 125 | 126 | export default formatter; -------------------------------------------------------------------------------- /www.phantom/src/service/plugin/phantom.js: -------------------------------------------------------------------------------- 1 | import api from '@/service/safe/api'; 2 | 3 | const phantom = function() { 4 | // The following functions are for public use 5 | 6 | /** 7 | * @param event String 8 | * @param callback Function 9 | * @param priority Number 10 | * @returns void 11 | */ 12 | this.addObserver = function(event, callback, priority) { 13 | if (!this._allowedEvents.includes(event)) { 14 | throw Error("The following observer event is not implemented in the Phantom plugin library: " + (typeof event === "string" ? event : "Unknown (not a string)")); 15 | } 16 | 17 | if (typeof callback !== "function") { 18 | throw new Error("You must pass a callback function to phantom.addObserver"); 19 | } 20 | 21 | // Set an average priority if none is given and clamp it to between 0 and 255 22 | priority = typeof priority !== "number" ? 122 : Math.max(0, Math.min(255, priority)); 23 | 24 | if (!this._observers.hasOwnProperty(event)) { 25 | this._observers[event] = []; 26 | } 27 | 28 | this._observers[event].push({ 29 | priority: priority, 30 | callback: callback 31 | }); 32 | }; 33 | 34 | /** 35 | * Loads a file by name from the NRS we are currently modifying, or a different NRS if provided 36 | * 37 | * @returns {Promise} 38 | */ 39 | this.getFile = async function(file, nrs) { 40 | if (typeof nrs !== "string") { 41 | nrs = await api.getCurrentDomain(); 42 | } 43 | 44 | return await api.fetch(nrs + "/" + (file.replace(/^\/+/, ''))); 45 | }; 46 | 47 | /** 48 | * 49 | * @returns {Promise} 50 | */ 51 | this.getNRS = async function() { 52 | return api.getCurrentDomain(); 53 | }; 54 | 55 | /** 56 | * Updates a file by name from the NRS we are currently modifying, or a different NRS if provided 57 | * 58 | * @returns {Promise} 59 | */ 60 | this.updateFile = async function(file, content, nrs) { 61 | if (typeof nrs !== "string") { 62 | nrs = await api.getCurrentDomain(); 63 | } 64 | 65 | return await api.updateFile(content, nrs, file.replace(/^\/+/, ''), false); 66 | }; 67 | 68 | // The following functions are for internal use 69 | 70 | this._allowedEvents = ['preCompile', 'postCompile']; 71 | this._observers = {}; 72 | 73 | this._processEvent = async function(observerEvent, data) { 74 | if (this._observers.hasOwnProperty(observerEvent)) { 75 | let observers = this._observers[observerEvent].sort((a, b) => b.priority - a.priority); 76 | 77 | for (let i = 0; i < observers.length; i++) { 78 | data = await observers[i].callback(data); 79 | } 80 | } 81 | 82 | return data; 83 | }; 84 | }; 85 | 86 | export default new phantom(); -------------------------------------------------------------------------------- /www.phantom/src/service/safe/api.js: -------------------------------------------------------------------------------- 1 | import auth from './lib/auth'; 2 | import nrs from './lib/nrs'; 3 | import files from './lib/files'; 4 | 5 | const api = function() { 6 | this.getAuthToken = auth.getAuthToken; 7 | this.authenticate = auth.authenticate; 8 | this.logout = auth.logout; 9 | this.getCurrentDomain = nrs.getCurrentLocalContainer; 10 | this.setCurrentDomain = nrs.setCurrentLocalContainer; 11 | this.getDomains = nrs.getLocalContainers; 12 | this.createDomain = nrs.createContainer; 13 | this.setTheme = nrs.setTheme; 14 | this.getTheme = nrs.getTheme; 15 | this.getInstalledThemes = files.getInstalledThemes; 16 | this.addInstalledTheme = files.addInstalledTheme; 17 | this.getThemeConfig = files.getThemeConfig; 18 | this.setThemeConfig = files.setThemeConfig; 19 | this.createContainer = files.createContainer; 20 | this.updateFile = files.updateFile; 21 | this.updateRawFile = files.updateRawFile; 22 | this.addGenericDocument = files.addGenericDocument; 23 | this.updateGenericDocument = files.updateGenericDocument; 24 | this.getGenericDocuments = files.getGenericDocuments; 25 | this.getGenericDocument = files.getGenericDocument; 26 | this.fetch = files.fetch; 27 | }; 28 | 29 | export default new api(); -------------------------------------------------------------------------------- /www.phantom/src/service/safe/cache/cache.js: -------------------------------------------------------------------------------- 1 | let getTimestamp = function() { 2 | // Standardise time to seconds since local epoch, should be supported by everything since IE3 3 | return (new Date()).getTime()/1000|0; 4 | }; 5 | 6 | const cache = { 7 | /** 8 | * Usage: 9 | * cache.get("example-key", safe.auth_app, [appInfo.id, appInfo.name, appInfo.vendor], ?300) 10 | * .then(response => {}) 11 | * .catch(err => {}); 12 | * 13 | * @param key {string} 14 | * @param callback {function} The function to call on cache MISS 15 | * @param callbackData {array} This will be passed to the callback via the spread operator 16 | * @param expires {number} Default expiry time for a GET is 1 year, expiry time is refreshed on a successful GET 17 | * @returns {Promise} 18 | */ 19 | get: function(key, callback, callbackData, expires) { 20 | return new Promise(function (resolve, reject) { 21 | let data = localStorage.getItem(key); 22 | 23 | if (data) { 24 | data = JSON.parse(data); 25 | 26 | if (parseInt(data.expires) >= parseInt(getTimestamp())) { 27 | // Refresh the cache on a successful HIT 28 | data.expires = getTimestamp() + parseInt(data.expiryLength); 29 | localStorage.setItem(key, JSON.stringify(data)); 30 | 31 | return resolve(data.data); 32 | } 33 | 34 | // Cache has expired, remove it 35 | localStorage.removeItem(key); 36 | } 37 | 38 | // On cache MISS or expiry 39 | return callback().then(response => { 40 | let expiryLength = (typeof expires === "number" ? expires : 31536000); 41 | 42 | let data = { 43 | data: response, 44 | expires: getTimestamp() + expiryLength, 45 | expiryLength: expiryLength 46 | }; 47 | 48 | localStorage.setItem(key, JSON.stringify(data)); 49 | return resolve(response); 50 | }); 51 | }) 52 | }, 53 | 54 | set(key, value, expires) { 55 | let expiryLength = (typeof expires === "number" ? expires : 31536000); 56 | 57 | let data = { 58 | data: value, 59 | expires: getTimestamp() + expiryLength, 60 | expiryLength: expiryLength 61 | }; 62 | 63 | localStorage.setItem(key, JSON.stringify(data)); 64 | }, 65 | 66 | /** 67 | * Usage: 68 | * cache.expire("example-key") 69 | * .then(_ => {}); 70 | * 71 | * It is not possible for this function to throw an Error in the targeted SAFE browsers, so no catch block is necessary 72 | * 73 | * @param key {string} 74 | * @returns {Promise} 75 | */ 76 | expire: function(key) { 77 | return new Promise(function (resolve, reject) { 78 | let data = localStorage.removeItem(key); 79 | return resolve(true); 80 | }); 81 | } 82 | }; 83 | 84 | export default cache; -------------------------------------------------------------------------------- /www.phantom/src/service/safe/lib/auth.js: -------------------------------------------------------------------------------- 1 | import promise from "../promise"; 2 | import logger from "@/service/log/logger"; 3 | 4 | const appInfo = { 5 | id: "app.SAFEPublishing.phantom", 6 | name: "Phantom", 7 | vendor: "SAFE Publishing" 8 | }; 9 | 10 | const auth = function () { 11 | this.getAuthToken = function() { 12 | return promise(async function(ctx) { 13 | return ctx.cache.get("auth", async function () { 14 | return false 15 | }); 16 | }); 17 | }; 18 | 19 | this.authenticate = function(token) { 20 | return promise(async function(ctx) { 21 | logger.createEvent('auth', token ? 'Loaded auth token from cache' : 'Requested new authentication token from auth_app'); 22 | return token ? token : ctx.safe.auth_app(appInfo.id, appInfo.name, appInfo.vendor); 23 | }).then(response => promise(async function(ctx) { 24 | logger.createEvent('auth', 'Attempted to connect to network'); 25 | // In the SAFE browser, this returns "undefined" on success 26 | await ctx.safe.connect(appInfo.id, response); 27 | ctx.cache.set("auth", response); 28 | logger.createEvent('auth', 'Connected to network'); 29 | return true; 30 | })) 31 | }; 32 | 33 | this.logout = function() { 34 | return promise(async function(ctx) { 35 | logger.createEvent('auth', 'Deleted authentication token'); 36 | return ctx.cache.set("auth", false); 37 | }); 38 | }; 39 | }; 40 | 41 | export default new auth(); -------------------------------------------------------------------------------- /www.phantom/src/service/safe/lib/files.js: -------------------------------------------------------------------------------- 1 | import promise from "../promise"; 2 | import Theme from '@/service/theme/theme'; 3 | 4 | const nrs = function (callback) { 5 | this.createContainer = function() { 6 | return promise(async function(ctx) { 7 | return ctx.safe.files_container_create_empty() 8 | }); 9 | }; 10 | 11 | /** 12 | * This is to be used when we're dealing with text content 13 | */ 14 | this.updateFile = function(content, filesContainerXorURL, path, updateNRS) { 15 | return promise(async function(ctx) { 16 | let buffer = content.toBuffer(); 17 | return ctx.safe.files_container_add_from_raw(buffer, filesContainerXorURL + '/' + path, true, updateNRS, false); 18 | }); 19 | }; 20 | 21 | /** 22 | * This is to be used when we are dealing exclusively with ArrayBuffer / UInt8Array 23 | */ 24 | this.updateRawFile = function(buffer, filesContainerXorURL, path, updateNRS) { 25 | return promise(async function(ctx) { 26 | return ctx.safe.files_container_add_from_raw(buffer, filesContainerXorURL + '/' + path, true, updateNRS, false); 27 | }); 28 | }; 29 | 30 | this.getGenericDocuments = function(nrs, type) { 31 | return promise(async function(ctx) { 32 | let documents = await ctx.cache.get(nrs + "/" + type, async function() { return []; }); 33 | 34 | return documents.sort((a, b) => { 35 | return new Date(b.modified) - new Date(a.modified); 36 | }); 37 | }) 38 | }; 39 | 40 | /** 41 | * The file isn't extracted from the document because it allows us to update posts when their file has been updated 42 | */ 43 | this.updateGenericDocument = function(nrs, file, document, type) { 44 | return promise(async function(ctx) { 45 | let documents = await ctx.cache.get(nrs + "/" + type, async function() { return []; }); 46 | 47 | for (let i = 0; i < documents.length; i++) { 48 | if (documents[i].file === file) { 49 | documents[i] = document; 50 | ctx.cache.set(nrs + "/" + type, documents); 51 | return true; 52 | } 53 | } 54 | 55 | return false; 56 | }) 57 | }; 58 | 59 | this.getGenericDocument = function(nrs, file, type) { 60 | return promise(async function(ctx) { 61 | let documents = await ctx.cache.get(nrs + "/" + type, async function() { return []; }); 62 | 63 | for (let i = 0; i < documents.length; i++) { 64 | if (documents[i].file === file) { 65 | return documents[i] 66 | } 67 | } 68 | 69 | return false; 70 | }) 71 | }; 72 | 73 | this.addGenericDocument = function(nrs, data, type) { 74 | return promise(async function(ctx) { 75 | let documents = await ctx.cache.get(nrs + "/" + type, async function() { return []; }); 76 | documents.push(data); 77 | ctx.cache.set(nrs + '/' + type, documents); 78 | return documents; 79 | }); 80 | }; 81 | 82 | this.fetch = function(url) { 83 | return promise(async function(ctx) { 84 | return ctx.safe.fetch(url); 85 | }); 86 | }; 87 | 88 | this.getInstalledThemes = function() { 89 | return promise(async function(ctx) { 90 | let themes = await ctx.cache.get("themes", async function() { return []; }); 91 | 92 | for (let i = 0; i < themes.length; i++) { 93 | themes[i] = new Theme(themes[i].config); 94 | } 95 | 96 | return themes; 97 | }) 98 | }; 99 | 100 | this.addInstalledTheme = function(theme) { 101 | let parent = this; 102 | 103 | return promise(async function(ctx) { 104 | let themes = await parent.getInstalledThemes(); 105 | 106 | for (let i = 0; i < themes.length; i++) { 107 | if (themes[i].config.name === theme.config.name) { 108 | themes[i] = theme; 109 | ctx.cache.set("themes", themes); 110 | return themes; 111 | } 112 | } 113 | 114 | themes.push(theme); 115 | ctx.cache.set("themes", themes); 116 | return themes; 117 | }); 118 | }; 119 | 120 | this.getThemeConfig = function(nrs, theme) { 121 | return promise(async function(ctx) { 122 | return ctx.cache.get(nrs + "/theme/" + theme + "/config", async function() { return {}; }); 123 | }) 124 | }; 125 | 126 | this.setThemeConfig = function(nrs, theme, value) { 127 | return promise(async function(ctx) { 128 | ctx.cache.set(nrs + "/theme/" + theme + "/config", value); 129 | }); 130 | } 131 | }; 132 | 133 | export default new nrs(); -------------------------------------------------------------------------------- /www.phantom/src/service/safe/lib/nrs.js: -------------------------------------------------------------------------------- 1 | import promise from "../promise"; 2 | import files from "./files"; 3 | 4 | async function returnEmptyArray() { 5 | return []; 6 | } 7 | 8 | const nrs = function (callback) { 9 | this.getCurrentLocalContainer = function() { 10 | return promise(async function(ctx) { 11 | return ctx.cache.get("current-nrs", async function() { return false; }); 12 | }) 13 | }; 14 | 15 | this.setCurrentLocalContainer = function(domain) { 16 | return promise(async function(ctx) { 17 | return ctx.cache.set("current-nrs", domain); 18 | }) 19 | }; 20 | 21 | 22 | this.getLocalContainers = function() { 23 | return promise(async function(ctx) { 24 | return ctx.cache.get("nrs", returnEmptyArray); 25 | }) 26 | }; 27 | 28 | this.createContainer = function(publicName, filesContainerXorURL) { 29 | return promise(async function(ctx) { 30 | let data = await ctx.safe.nrs_map_container_create(publicName, filesContainerXorURL, true, true, false); 31 | let existingData = await ctx.cache.get("nrs", returnEmptyArray); 32 | existingData.push(data); 33 | ctx.cache.set("nrs", existingData); 34 | return data; 35 | }) 36 | }; 37 | 38 | this.setTheme = function(publicName, theme) { 39 | return promise(async function(ctx) { 40 | return ctx.cache.set(publicName + '/theme', theme); 41 | }) 42 | }; 43 | 44 | this.getTheme = function(publicName) { 45 | return promise(async function(ctx) { 46 | let themes = await files.getInstalledThemes(), 47 | activeTheme = await ctx.cache.get(publicName + '/theme', async function() { return "Zen"; }); 48 | 49 | for (let i = 0; i < themes.length; i++) { 50 | if (themes[i].config.name === activeTheme) { 51 | return themes[i]; 52 | } 53 | } 54 | 55 | throw new Error("No theme is currently installed"); 56 | }) 57 | } 58 | }; 59 | 60 | export default new nrs(); -------------------------------------------------------------------------------- /www.phantom/src/service/safe/mock/safe.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file mocks the window.Safe library calls, for testing on clearnet websites or when the SAFE network app has 3 | * bugs which prevent us from authenticating. I encourage the usage of this file outside of this project and as such 4 | * it has 0 dependencies and can be readily copy-and-pasted 5 | * 6 | * @author Shane Armstrong 7 | * @licence MIT 8 | * @package Phantom 9 | */ 10 | import files from "../lib/files"; 11 | 12 | // To disable the injection of random network errors, set this to false 13 | const throwErrors = true; 14 | 15 | function throwErrorRandomly() { 16 | if (!throwErrors) { 17 | return; 18 | } 19 | 20 | if (Math.round(Math.random() * 10) === 0) { 21 | throw "Injected a random NEON error, make sure your code handles unexpected network error states"; 22 | } 23 | } 24 | 25 | function getRandomMockXorURL() { 26 | let generation = function() { 27 | return Math.random().toString(36).substr(2, 10); 28 | }; 29 | 30 | return btoa(generation() + generation() + generation()); 31 | } 32 | 33 | export default { 34 | // This generates an auth string which can be passed to `connect(app_id, credentials)` as the second parameter 35 | auth_app(id, name, vendor) { 36 | throwErrorRandomly(); 37 | return btoa(id + '/' + name + '/' + vendor); 38 | }, 39 | 40 | // In the SAFE network, this returns Promise{undefined} on success and throws a catchable error on failure 41 | connect(app_id, credentials) { 42 | throwErrorRandomly(); 43 | return undefined; 44 | }, 45 | 46 | files_container_create_empty() { 47 | throwErrorRandomly(); 48 | return getRandomMockXorURL(); 49 | }, 50 | 51 | files_container_add_from_raw(buffer, path, force, updateNRS, dryRun) { 52 | throwErrorRandomly(); 53 | 54 | let current = (new Date()).toISOString(), 55 | resource = "safe://" + getRandomMockXorURL(), 56 | nameDefinition = {}, 57 | filesDefinition = {}, 58 | relativePath = path.split("/"); 59 | relativePath.shift(); 60 | relativePath = relativePath.join("/"); 61 | 62 | // This empty string is on purpose, this is how the SAFE browser currently returns the URIs when you're updating an NRS URL file 63 | nameDefinition[""] = ["*", resource]; 64 | filesDefinition[relativePath] = {created: current, link: resource, modified: current, size: buffer.length, type: "Raw"}; 65 | 66 | return [ 67 | 1, //version 68 | nameDefinition, 69 | filesDefinition 70 | ]; 71 | }, 72 | 73 | nrs_map_container_create(publicName, filesContainerXorURL, defaultContainer, hardLink, dryRun) { 74 | throwErrorRandomly(); 75 | 76 | let current = (new Date()).toISOString(), 77 | nameDefinition = {}; 78 | nameDefinition[publicName] = ["+", filesContainerXorURL]; 79 | 80 | return [ 81 | filesContainerXorURL.split("?")[0], 82 | nameDefinition, 83 | { 84 | default: { 85 | OtherRdf: { 86 | created: current, 87 | link: filesContainerXorURL, 88 | modified: current 89 | } 90 | } 91 | } 92 | ]; 93 | }, 94 | 95 | fetch(url) { 96 | return window.fetch(url); 97 | } 98 | } -------------------------------------------------------------------------------- /www.phantom/src/service/safe/promise.js: -------------------------------------------------------------------------------- 1 | import mockSafe from './mock/safe'; 2 | import cache from './cache/cache'; 3 | let safe = typeof window.Safe !== "undefined" ? (new window.Safe()) : mockSafe; 4 | 5 | export default function(callback) { 6 | return new Promise((resolve, reject) => { 7 | try{ 8 | let context = { 9 | 'safe': safe, 10 | 'cache': cache 11 | }; 12 | 13 | const result = callback(context); 14 | resolve(result) 15 | }catch(error) { 16 | reject(error); 17 | } 18 | }); 19 | } -------------------------------------------------------------------------------- /www.phantom/src/service/theme/importer.js: -------------------------------------------------------------------------------- 1 | import api from '@/service/safe/api'; 2 | import Theme from '@/service/theme/theme'; 3 | 4 | const importer = { 5 | import: function(url, translations, translate) { 6 | return api.fetch(url).then(config => { 7 | return config.text(); 8 | }).then(async function(config) { 9 | let theme = new Theme(JSON.parse(config)); 10 | 11 | if (typeof theme.config.parent === "undefined") { 12 | // This triggers internationalisation and a few other nice-to-haves 13 | let mockParentTheme = new Theme(JSON.parse(config)); 14 | mockParentTheme.config = {}; 15 | await theme.mergeConfig(mockParentTheme, translations, translate); 16 | } else { 17 | while (typeof theme.config.parent === "string") { 18 | let parentTheme = new Theme(JSON.parse(await (await api.fetch(theme.config.parent)).text())); 19 | await theme.mergeConfig(parentTheme, translations, translate); 20 | } 21 | } 22 | 23 | theme.lintThemeConfig(); 24 | theme.config.origin = url; 25 | 26 | return api.addInstalledTheme(theme); 27 | }).catch(err => { 28 | alert("Unable to load theme file, with error: " + err.message); 29 | }); 30 | } 31 | }; 32 | 33 | export default importer; -------------------------------------------------------------------------------- /www.phantom/src/service/theme/readme.md: -------------------------------------------------------------------------------- 1 | ### Default themes 2 | 3 | The default built-in themes do not use enheritance. The long and the short of it is that it would be a pain and it's easier for us to simply compose our themes ahead of time. -------------------------------------------------------------------------------- /www.phantom/src/service/theme/theme.js: -------------------------------------------------------------------------------- 1 | import api from '@/service/safe/api'; 2 | import canonical from '@/service/markdown/canonical'; 3 | import formatter from '@/service/markdown/formatter'; 4 | import phantomPluginTools from '@/service/plugin/phantom'; 5 | 6 | const Theme = function(config) { 7 | this.config = config; 8 | 9 | this.getComputedTemplate = function (domain) { 10 | let parent = this; 11 | 12 | return api.fetch(this.config.template).then(response => { 13 | return response.text(); 14 | }).then(async function (template) { 15 | let scriptData = "", 16 | styleData = "", 17 | config = await api.getThemeConfig(domain, parent.config.name), 18 | documentGroups = [{name: 'posts', prefix: '/post'}, { name: 'pages', prefix: ''}]; 19 | 20 | for (let i = 0; i < documentGroups.length; i++) { 21 | documentGroups[i].documents = await parent.getGenericBundle(domain, documentGroups[i].name, documentGroups[i].prefix) 22 | } 23 | 24 | // Before we compile, fire off an event to let modules modify the data 25 | let data = await phantomPluginTools._processEvent('preCompile', { 26 | documentGroups: documentGroups, 27 | scripts: [].concat(parent.config.scripts), 28 | styles: [].concat(parent.config.styles), 29 | config: config 30 | }); 31 | 32 | for (let i = 0; i < data.scripts.length; i++) { 33 | scriptData += await (await api.fetch(data.scripts[i])).text(); 34 | } 35 | 36 | for (let i = 0; i < data.styles.length; i++) { 37 | styleData += await (await api.fetch(data.styles[i])).text(); 38 | } 39 | 40 | let documentData = ''; 41 | 42 | data.documentGroups.forEach(group => { 43 | documentData += 'window.' + group.name + ' = ' + JSON.stringify(group.documents) + '; '; 44 | }); 45 | 46 | let compiled = template 47 | .replace(/<\/head>/g, '') 48 | .replace(/<\/body>/g, '') 49 | .replace(/<\/body>/g, ''); 50 | 51 | // Now that we've compiled, fire off an event to let modules modify the compiled template before deployment 52 | return (await phantomPluginTools._processEvent('postCompile', { document: compiled })).document; 53 | }) 54 | }; 55 | 56 | this.getGenericBundle = async function (domain, type, prefix) { 57 | let documents = await api.getGenericDocuments(domain, type), 58 | response = []; 59 | 60 | documents = documents.sort((a, b) => { 61 | return new Date(b.created) - new Date(a.created); 62 | }); 63 | 64 | for (let i = 0; i < documents.length; i++) { 65 | if (documents[i].state === "draft") { 66 | documents[i].state = "published"; 67 | await api.updateGenericDocument(domain, documents[i].file, documents[i], type); 68 | } 69 | 70 | if (documents[i].state === "published") { 71 | let markdown = canonical.getMarkdownFromHTML(documents[i].data); 72 | 73 | response.push({ 74 | path: prefix + '/' + documents[i].file, 75 | title: formatter.getTitle(markdown), 76 | excerpt: formatter.getParsedHTML(formatter.getExcerpt(markdown), true), // The title has already been stripped out 77 | template: formatter.getParsedHTML(markdown, true), 78 | created: documents[i].created 79 | }); 80 | } 81 | } 82 | 83 | return response; 84 | }; 85 | 86 | /** 87 | * This gives themes a concept of inheritance, merging applicable configuration fields to build up a final super-theme! 88 | * 89 | * @param theme Theme 90 | * @param translations Object 91 | * @param translate Function 92 | */ 93 | this.mergeConfig = async function(theme, translations, translate) { 94 | let a = this.config, 95 | b = theme.config; 96 | 97 | // Only in the case of the parent field do we prioritise the parent theme, this allows us to do fancy inheritance stuff 98 | a.parent = typeof b.parent === "string" ? b.parent : false; 99 | 100 | // For everything else, prioritise the child theme 101 | a.name = typeof a.name === "string" ? a.name : b.name; 102 | a.banner = typeof a.banner === "string" ? a.banner : b.banner; 103 | a.template = typeof a.template === "string" ? a.template : b.template; 104 | 105 | // After this point, translations are available 106 | a.locales = typeof a.locales === "undefined" ? {} : a.locales; 107 | b.locales = typeof b.locales === "undefined" ? {} : b.locales; 108 | await this.mergeLocales(b, translations); 109 | 110 | this.mergeConfigArrays(b, "scripts"); 111 | this.mergeConfigArrays(b, "styles"); 112 | this.mergeConfigArrays(b, "config"); 113 | a.description = typeof a.description === "string" ? a.description : b.description; 114 | }; 115 | 116 | this.mergeConfigArrays = function(parentTheme, index) { 117 | parentTheme[index] = Array.isArray(parentTheme[index]) ? parentTheme[index] : []; 118 | this.config[index] = Array.isArray(this.config[index]) ? parentTheme[index].concat(this.config[index]) : parentTheme[index]; 119 | }; 120 | 121 | this.mergeLocales = async function(parentTheme, translations) { 122 | if (typeof this.config.localeTranslations === "undefined") { 123 | this.config.localeTranslations = {}; 124 | 125 | if (typeof this.config.locales !== "undefined") { 126 | await this.importLocales(this.config.locales); 127 | } 128 | } 129 | 130 | if (typeof parentTheme.config !== "undefined" && typeof parentTheme.config.locales !== "undefined") { 131 | await this.importLocales(parentTheme.config.locales); 132 | } 133 | 134 | for (let locale in this.config.localeTranslations) { 135 | if (this.config.localeTranslations.hasOwnProperty(locale)) { 136 | if (typeof translations[locale] === "undefined") { 137 | translations[locale] = {}; 138 | } 139 | 140 | for (let key in this.config.localeTranslations[locale]) { 141 | if (this.config.localeTranslations[locale].hasOwnProperty(key)) { 142 | if (typeof translations[locale][key] === "undefined") { 143 | translations[locale][key] = this.config.localeTranslations[locale][key]; 144 | } 145 | } 146 | } 147 | } 148 | } 149 | }; 150 | 151 | this.importLocales = async function(locales) { 152 | for (let key in locales) { 153 | if (locales.hasOwnProperty(key)) { 154 | if (typeof this.config.localeTranslations[key] === "undefined") { 155 | this.config.localeTranslations[key] = {}; 156 | } 157 | 158 | let translations = await (await api.fetch(locales[key])).text(); 159 | let translationObject = JSON.parse(translations); 160 | 161 | for (let translation in translationObject) { 162 | if (translationObject.hasOwnProperty(translation)) { 163 | this.config.localeTranslations[key]["_" + this.config.name + "_" + translation] = translationObject[translation]; 164 | } 165 | } 166 | } 167 | } 168 | }; 169 | 170 | this.translate = function(key, translate) { 171 | return translate("_" + this.config.name + "_" + key, key); 172 | }; 173 | 174 | /** 175 | * Tests the config for any errors or missing fields 176 | * 177 | * @return boolean 178 | * @throws Error 179 | */ 180 | this.lintThemeConfig = function() { 181 | this.assert(typeof this.config.name === "string" && this.config.name.length, "The theme name must be a string with at least one character"); 182 | this.assert(typeof this.config.description === "string" && this.config.description.length, "The theme description must be a string with at least one character"); 183 | this.assert(typeof this.config.banner === "string" && this.config.banner.match(/\.(png|jpeg|jpg|gif)$/), "The theme banner must point to a file ending in .png|.jpeg|.jpg|.gif"); 184 | this.assert(typeof this.config.template === "string" && this.config.template.match(/\.(html)$/), "The theme template must point to a file ending in .html"); 185 | this.assert(Array.isArray(this.config.scripts) && this.config.scripts.length, "The theme must import at least one script file"); 186 | this.assert(Array.isArray(this.config.styles) && this.config.styles.length, "The theme must import at least one css file"); 187 | 188 | if (typeof this.config.config !== "undefined") { 189 | this.assert(Array.isArray(this.config.config) && this.config.config.length, "The theme config (if included) must be an array containing at least one object"); 190 | 191 | this.config.config.forEach((item, i) => { 192 | this.assert(typeof item.name === "string" && item.name.length, "The theme config (index: " + i + ") name must be a string with at least one character"); 193 | this.assert(typeof item.description === "string" && item.description.length, "The theme config (index: " + i + ", name: " + item.name + ") description must be a string with at least one character"); 194 | this.assert((["single", "multi"].includes(item.count)), "The theme config (index: " + i + ", name: " + item.name + ") count must be a string with the value single|multi"); 195 | this.assert(Array.isArray(item.fields) && item.fields.length, "The theme config (index: " + i + ", name: " + item.name + ") fields must be an array containing at least one object"); 196 | 197 | item.fields.forEach((field, v) => { 198 | this.assert(typeof field.name === "string" && field.name.length, "The theme config field (config index: " + i + ", field index: " + v + ") name must be a string with at least one character"); 199 | this.assert(typeof field.description === "string" && field.description.length, "The theme config field (config index: " + i + ", field name: " + field.name + ") description must be a string with at least one character"); 200 | this.assert((["text", "email", "date", "number", "tel", "password", "time", "file"].includes(field.type)), "The theme config field (config index: " + i + ", field name: " + field.name + ") type must be a string with the value text|email|date|number|tel|password|time|file"); 201 | }); 202 | }); 203 | } 204 | }; 205 | 206 | /** 207 | * @param condition boolean 208 | * @param message string 209 | * @throws Error 210 | */ 211 | this.assert = function(condition, message) { 212 | if (!condition) { 213 | throw new Error(message); 214 | } 215 | }; 216 | }; 217 | 218 | export default Theme; -------------------------------------------------------------------------------- /www.phantom/src/view/About.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | 20 | -------------------------------------------------------------------------------- /www.phantom/src/view/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 39 | 40 | -------------------------------------------------------------------------------- /www.phantom/src/view/Dev/Log.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 70 | 71 | -------------------------------------------------------------------------------- /www.phantom/src/view/Docs/Home.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /www.phantom/src/view/Docs/Wrapper.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 25 | 26 | -------------------------------------------------------------------------------- /www.phantom/src/view/DomainCreate.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 48 | 49 | -------------------------------------------------------------------------------- /www.phantom/src/view/Domains.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 101 | 102 | -------------------------------------------------------------------------------- /www.phantom/src/view/Home.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 60 | 61 | -------------------------------------------------------------------------------- /www.phantom/src/view/PageEdit.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /www.phantom/src/view/Pages.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /www.phantom/src/view/PostEdit.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /www.phantom/src/view/Posts.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /www.phantom/src/view/Theme.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 138 | 139 | -------------------------------------------------------------------------------- /www.phantom/src/view/Themes.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 114 | 115 | -------------------------------------------------------------------------------- /www.phantom/vue.config.js: -------------------------------------------------------------------------------- 1 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 2 | const fs = require('fs'); 3 | 4 | module.exports = { 5 | devServer: { 6 | host: '127.0.0.1', 7 | port: 5000, 8 | disableHostCheck: true, 9 | clientLogLevel: 'info', 10 | hot: true, 11 | watchOptions: { 12 | poll: true, 13 | ignored: [/node_modules/, /dist/] 14 | } 15 | }, 16 | configureWebpack: { 17 | optimization: { 18 | minimizer: [new UglifyJsPlugin({ 19 | cache: true, 20 | parallel: true, 21 | uglifyOptions: { 22 | compress: true, 23 | ecma: 6, 24 | mangle: true, 25 | output: { 26 | comments: false 27 | } 28 | }, 29 | sourceMap: true 30 | })], 31 | }, 32 | }, 33 | chainWebpack: (config) => { 34 | config.plugins.delete('prefetch') 35 | }, 36 | lintOnSave: false, 37 | runtimeCompiler: false, 38 | productionSourceMap: true 39 | }; --------------------------------------------------------------------------------