├── .gitignore
├── LICENSE
├── README.md
├── firebase.json
├── functions
├── package-lock.json
├── package.json
└── src
│ ├── attachUserToProject.js
│ ├── createUser.js
│ ├── deleteProject.js
│ ├── index.js
│ ├── initSettings.js
│ └── publishJson.js
├── package.json
├── public
├── favicon.ico
├── index.html
├── powered-by-editlayer.png
└── powered-by-editlayer.svg
├── rules
├── cors.json
├── firestore.rules
└── storage.rules
├── scripts
├── cors.js
├── deploy.js
└── open.js
└── src
├── App.vue
├── assets
└── image-background.png
├── components
├── SideItem.vue
├── content
│ ├── Editor.vue
│ ├── PartnerInfo.vue
│ └── PublishButton.vue
├── core
│ ├── Alert.vue
│ ├── Button.vue
│ ├── Card.vue
│ ├── Heading.vue
│ ├── Notification.vue
│ └── registerAll.js
├── dashboard
│ ├── Navigation.vue
│ ├── NewProjectButton.vue
│ ├── NewProjectModal.vue
│ └── Projects.vue
├── navigation
│ ├── BackButton.vue
│ ├── Breadcrumb.vue
│ └── Navigation.vue
├── panel
│ ├── Item.vue
│ ├── ItemsFromArray.vue
│ ├── ItemsFromObject.vue
│ └── Panel.vue
├── schema
│ └── SchemaEditor.vue
├── settings
│ ├── DeleteProject.vue
│ ├── FileLocation.vue
│ ├── NewUserButton.vue
│ ├── NewUserModal.vue
│ ├── Users.vue
│ └── Webhook.vue
└── utils
│ ├── FooterContent.vue
│ ├── LoaderOverlay.vue
│ └── Notifications.vue
├── editors
├── Calendar.vue
├── Checkbox.vue
├── Code.vue
├── Color.vue
├── Image.vue
├── Markdown.vue
├── Number.vue
├── Radio.vue
├── RichText.vue
├── Select.vue
├── SelectImage.vue
├── Text.vue
├── Textarea.vue
├── Video.vue
├── common
│ ├── BaseEditor.vue
│ └── BasePreview.vue
├── config.js
└── previews
│ ├── Array.vue
│ ├── Calendar.vue
│ ├── Code.vue
│ ├── Color.vue
│ ├── Image.vue
│ ├── Option.vue
│ ├── Text.vue
│ └── Video.vue
├── main.js
├── router.js
├── sass
├── codemirror.sass
├── core
│ ├── _breakpoint.sass
│ ├── _center.sass
│ ├── _chain.sass
│ ├── _container.sass
│ ├── _gap.sass
│ ├── _grid.sass
│ ├── _hx.sass
│ ├── _invert.sass
│ ├── _mask.sass
│ ├── initialize.sass
│ └── mixins.sass
├── transitions.sass
└── variables.sass
├── store
├── index.js
└── modules
│ ├── auth.js
│ ├── content.js
│ ├── firestore.js
│ ├── notifications.js
│ ├── projects.js
│ ├── schema.js
│ ├── structure.js
│ ├── uploader.js
│ └── utils.js
├── utils
├── buildJson.js
├── buildStructure.js
├── codemirror.js
├── firebase.js
├── icons.js
└── webhook.js
└── views
├── Auth.vue
├── Content.vue
├── Dashboard.vue
├── Schema.vue
└── Settings.vue
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | dist
4 | __*
5 | .env*.local
6 | .firebaserc
7 | editalyer-archive.zip
8 | .firebase
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 |
15 | # Editor directories and files
16 | .idea
17 | .vscode
18 | *.suo
19 | *.ntvs*
20 | *.njsproj
21 | *.sln
22 | *.sw*
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Editlayer documentation
2 | https://editlayer.rane.io/documentation
3 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "firestore": {
3 | "rules": "rules/firestore.rules"
4 | },
5 | "hosting": {
6 | "public": "dist",
7 | "ignore": [
8 | "firebase.json",
9 | "**/.*",
10 | "**/node_modules/**"
11 | ],
12 | "rewrites": [
13 | {
14 | "source": "**",
15 | "destination": "/index.html"
16 | }
17 | ]
18 | },
19 | "storage": {
20 | "rules": "rules/storage.rules"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "editlayer-functions",
3 | "description": "Firebase Cloud Functions for Editlayer",
4 | "main": "dist/index.js",
5 | "dependencies": {
6 | "firebase-admin": "~8.4.0",
7 | "firebase-functions": "^3.2.0",
8 | "js-base64": "^2.5.1",
9 | "lodash": "^4.17.21"
10 | },
11 | "babel": {
12 | "presets": [
13 | [
14 | "env",
15 | {
16 | "targets": {
17 | "node": "6.14.0"
18 | }
19 | }
20 | ]
21 | ]
22 | },
23 | "private": true
24 | }
25 |
--------------------------------------------------------------------------------
/functions/src/attachUserToProject.js:
--------------------------------------------------------------------------------
1 | import admin from 'firebase-admin'
2 | import _ from 'lodash'
3 | import { Base64 } from 'js-base64'
4 |
5 | export default {
6 | async whenAdded (snap, context) {
7 | const firestore = admin.firestore()
8 | const auth = admin.auth()
9 | const jobData = snap.data()
10 |
11 | if (jobData.job !== 'attachUserToProject') return false
12 |
13 | const userId = await auth
14 | .getUserByEmail(jobData.email)
15 | .then(userRecord => userRecord.uid)
16 | .catch(error => {
17 | console.error(error)
18 | return false
19 | })
20 |
21 | if (userId === false) return false
22 |
23 | const project = await firestore
24 | .collection('projects')
25 | .doc(jobData.projectId)
26 | .get()
27 |
28 | const projectData = project.data()
29 | const user = projectData.users[jobData.awaitId]
30 |
31 | let updateData = {}
32 |
33 | // Create new user with real id
34 | updateData[`users.${userId}`] = {
35 | email: user.email,
36 | permissions: user.permissions,
37 | userExist: true,
38 | }
39 |
40 | // Remove await user
41 | updateData[`users.${jobData.awaitId}`] = admin.firestore.FieldValue.delete()
42 |
43 | firestore
44 | .collection('projects')
45 | .doc(context.params.projectId)
46 | .update(updateData)
47 |
48 | console.log('attachUserToProjectWhenAdded done')
49 | return true
50 | },
51 |
52 | whenRegister (userRecord, context) {
53 | const firestore = admin.firestore()
54 | const awaitId = `await-${Base64.encodeURI(userRecord.email)}`
55 |
56 | return firestore
57 | .collection('projects')
58 | .get()
59 | .then(querySnapshot => {
60 | querySnapshot.forEach((doc) => {
61 | if (_.has(doc.data().users, awaitId)) {
62 | const user = doc.data().users[awaitId]
63 | let updateData = {}
64 |
65 | // Create new user with real id
66 | updateData[`users.${userRecord.uid}`] = {
67 | email: user.email,
68 | permissions: user.permissions,
69 | userExist: true,
70 | }
71 |
72 | // Remove await user
73 | updateData[`users.${awaitId}`] = admin.firestore.FieldValue.delete()
74 |
75 | firestore
76 | .collection('projects')
77 | .doc(doc.id)
78 | .update(updateData)
79 | }
80 | })
81 |
82 | console.log('attachUserToProjectWhenRegister done')
83 | return true
84 | })
85 | .catch(error => console.error('User adding when register failed', error))
86 | },
87 | }
88 |
--------------------------------------------------------------------------------
/functions/src/createUser.js:
--------------------------------------------------------------------------------
1 | import admin from 'firebase-admin'
2 |
3 | export default async (userRecord, context) => {
4 | const firestore = admin.firestore()
5 |
6 | const settings = await firestore
7 | .collection('settings')
8 | .doc('general')
9 | .get()
10 | .then(snap => snap.data())
11 | .catch(error => console.error('Settings getting failed', error))
12 |
13 | const firstUser = await firestore
14 | .collection('users')
15 | .get()
16 | .then(snap => snap.size === 0)
17 | .catch(error => console.error('Users getting failed', error))
18 |
19 | const allowCreateProject = settings.allowNewUsersCreateProject === true || firstUser === true
20 |
21 | await firestore
22 | .collection('users')
23 | .doc(userRecord.uid)
24 | .set({
25 | email: userRecord.email,
26 | permissions: {
27 | createProject: allowCreateProject,
28 | },
29 | })
30 | .catch(error => console.error('User added failed', error))
31 |
32 | console.log('createUser done')
33 | return true
34 | }
35 |
--------------------------------------------------------------------------------
/functions/src/deleteProject.js:
--------------------------------------------------------------------------------
1 | import admin from 'firebase-admin'
2 |
3 | export default (snap, context) => {
4 | const firestore = admin.firestore()
5 | const bucket = admin.storage().bucket()
6 | const storeData = snap.data()
7 |
8 | if (storeData.job !== 'deleteProject') return false
9 | if (storeData.deleteProjectId !== context.params.projectId) return false
10 |
11 | return bucket
12 | .getFiles({ prefix: context.params.projectId })
13 | .then((result) => {
14 | let files = result[0]
15 |
16 | files.forEach((file) => {
17 | bucket.file(file.name).delete()
18 | })
19 |
20 | return true
21 | })
22 | .then(() => {
23 | return firestore
24 | .collection('projects')
25 | .doc(context.params.projectId)
26 | .collection('versions')
27 | .get()
28 | })
29 | .then((versions) => {
30 | versions.forEach(doc => {
31 | firestore
32 | .collection('projects')
33 | .doc(context.params.projectId)
34 | .collection('versions')
35 | .doc(doc.id)
36 | .delete()
37 | })
38 |
39 | return firestore
40 | .collection('projects')
41 | .doc(context.params.projectId)
42 | .collection('jobs')
43 | .get()
44 | })
45 | .then((jobs) => {
46 | jobs.forEach(doc => {
47 | firestore
48 | .collection('projects')
49 | .doc(context.params.projectId)
50 | .collection('jobs')
51 | .doc(doc.id)
52 | .delete()
53 | })
54 |
55 | console.log('deleteProject done')
56 | return firestore
57 | .collection('projects')
58 | .doc(context.params.projectId)
59 | .delete()
60 | })
61 | .catch(error => {
62 | console.log('Project deleting failed', error)
63 | return false
64 | })
65 | }
66 |
--------------------------------------------------------------------------------
/functions/src/index.js:
--------------------------------------------------------------------------------
1 | import * as functions from 'firebase-functions'
2 | import admin from 'firebase-admin'
3 | import publishJson from './publishJson'
4 | import attachUserToProject from './attachUserToProject'
5 | import createUser from './createUser'
6 | import deleteProject from './deleteProject'
7 | import initSettings from './initSettings'
8 |
9 | admin.initializeApp()
10 |
11 | exports.newUser = functions
12 | .auth
13 | .user()
14 | .onCreate(async (snap, context) => {
15 | await initSettings(snap, context)
16 | await createUser(snap, context)
17 | await attachUserToProject.whenRegister(snap, context)
18 | return true
19 | })
20 |
21 | exports.newVersion = functions
22 | .firestore
23 | .document('projects/{projectId}/versions/{versionId}')
24 | .onCreate(async (snap, context) => {
25 | await publishJson(snap, context)
26 | return true
27 | })
28 |
29 | exports.newJob = functions
30 | .firestore
31 | .document('projects/{projectId}/jobs/{jobId}')
32 | .onCreate(async (snap, context) => {
33 | await attachUserToProject.whenAdded(snap, context)
34 | await deleteProject(snap, context)
35 | return true
36 | })
37 |
--------------------------------------------------------------------------------
/functions/src/initSettings.js:
--------------------------------------------------------------------------------
1 | import admin from 'firebase-admin'
2 |
3 | export default async (userRecord, context) => {
4 | const firestore = admin.firestore()
5 |
6 | const isSettingsExsist = await firestore
7 | .collection('settings')
8 | .get()
9 | .then(snap => snap.size > 0)
10 | .catch(error => console.error('Settings failed', error))
11 |
12 | if (isSettingsExsist === false) {
13 | await firestore
14 | .collection('settings')
15 | .doc('general')
16 | .set({
17 | allowNewUsersCreateProject: false,
18 | })
19 | }
20 |
21 | console.log('initSettings done')
22 | return false
23 | }
24 |
--------------------------------------------------------------------------------
/functions/src/publishJson.js:
--------------------------------------------------------------------------------
1 | import admin from 'firebase-admin'
2 | import fs from 'fs'
3 | import _ from 'lodash'
4 |
5 | export default (snap, context) => {
6 | const bucket = admin.storage().bucket()
7 | const firestore = admin.firestore()
8 | const tempFilePath = '/tmp/tempfile.json'
9 | const versionData = snap.data()
10 | const destinationPath = `${context.params.projectId}/content.json`
11 |
12 | const jsonFileContent = _.merge(versionData.json, {
13 | PUBLISHED_AT: versionData.publishedAt,
14 | VERSION_ID: context.params.versionId,
15 | })
16 |
17 | const json = JSON.stringify(jsonFileContent, null, 2)
18 |
19 | fs.writeFileSync(tempFilePath, json)
20 |
21 | return bucket
22 | .upload(tempFilePath, {
23 | destination: destinationPath,
24 | metadata: {
25 | metadata: {
26 | firebaseStorageDownloadTokens: versionData.token,
27 | },
28 | },
29 | })
30 | .then(() => {
31 | console.log('JSON publishing done')
32 |
33 | let updateData = {}
34 | updateData[`publishedVersion`] = {
35 | draft: versionData.draft,
36 | schema: versionData.schema,
37 | json: json,
38 | publishedAt: versionData.publishedAt,
39 | versionId: context.params.versionId,
40 | }
41 |
42 | firestore
43 | .collection('projects')
44 | .doc(context.params.projectId)
45 | .update(updateData)
46 |
47 | console.log('publishJson done')
48 | return true
49 | })
50 | .catch((error) => console.error('JSON publishing failed', error.message))
51 | }
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "editlayer-cms",
3 | "description": "Editlayer is headless CMS that saves content as JSON",
4 | "version": "0.7.0",
5 | "private": true,
6 | "scripts": {
7 | "init": "npm install && cd functions && npm install",
8 | "serve": "vue-cli-service serve --port 8000 --open",
9 | "deploy": "node scripts/deploy.js",
10 | "cors": "node scripts/cors.js",
11 | "open": "node scripts/open.js",
12 | "archive": "git archive --format=zip --output=editlayer-archive.zip HEAD"
13 | },
14 | "dependencies": {
15 | "@jaames/iro": "^4.5.1",
16 | "animejs": "^3.1.0",
17 | "autosize": "^4.0.2",
18 | "axios": "^0.19.0",
19 | "blob-util": "^2.0.2",
20 | "caret-pos": "^1.2.1",
21 | "copy-to-clipboard": "^3.2.0",
22 | "dayjs": "^1.8.16",
23 | "firebase": "^6.5.0",
24 | "image-compressor.js": "^1.1.4",
25 | "is-upper-case": "^1.1.2",
26 | "js-base64": "^2.5.1",
27 | "js-video-url-parser": "^0.4.0",
28 | "lodash": "^4.17.15",
29 | "nanoid": "^2.1.0",
30 | "parse-json": "^5.0.0",
31 | "slugg": "^1.2.1",
32 | "title-case": "^2.1.1",
33 | "typeface-open-sans": "0.0.75",
34 | "v-calendar": "^0.9.7",
35 | "vue": "^2.6.10",
36 | "vue-awesome": "^3.5.4",
37 | "vue-codemirror": "^4.0.6",
38 | "vue-quill-editor": "^3.0.6",
39 | "vue-router": "^3.1.3",
40 | "vuex": "^3.1.1",
41 | "vuex-router-sync": "^5.0.0"
42 | },
43 | "devDependencies": {
44 | "@vue/cli": "^3.11.0",
45 | "@vue/cli-plugin-babel": "^3.11.0",
46 | "@vue/cli-plugin-eslint": "^3.11.0",
47 | "@vue/cli-service": "^3.11.0",
48 | "@vue/eslint-config-standard": "^4.0.0",
49 | "babel-cli": "^6.26.0",
50 | "babel-core": "7.0.0-bridge.0",
51 | "babel-preset-env": "^1.7.0",
52 | "firebase-tools": "^7.3.0",
53 | "node-sass": "^4.12.0",
54 | "open": "^6.4.0",
55 | "sass-loader": "^8.0.0",
56 | "vue-template-compiler": "^2.6.10"
57 | },
58 | "babel": {
59 | "presets": [
60 | "@vue/app"
61 | ]
62 | },
63 | "eslintConfig": {
64 | "root": true,
65 | "env": {
66 | "node": true
67 | },
68 | "extends": [
69 | "plugin:vue/essential",
70 | "@vue/standard"
71 | ],
72 | "rules": {
73 | "comma-dangle": [
74 | "error",
75 | "always-multiline"
76 | ],
77 | "brace-style": [
78 | "error",
79 | "stroustrup"
80 | ]
81 | },
82 | "parserOptions": {
83 | "parser": "babel-eslint"
84 | }
85 | },
86 | "postcss": {
87 | "plugins": {
88 | "autoprefixer": {}
89 | }
90 | },
91 | "browserslist": [
92 | "> 1%",
93 | "last 2 versions",
94 | "not ie <= 8"
95 | ]
96 | }
97 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/raneio/editlayer/9fe46e34377c13a6626780acc98971eb023eca5a/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Editlayer
9 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/public/powered-by-editlayer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/raneio/editlayer/9fe46e34377c13a6626780acc98971eb023eca5a/public/powered-by-editlayer.png
--------------------------------------------------------------------------------
/rules/cors.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "origin": ["*"],
4 | "method": ["GET"],
5 | "maxAgeSeconds": 3600
6 | }
7 | ]
8 |
--------------------------------------------------------------------------------
/rules/firestore.rules:
--------------------------------------------------------------------------------
1 | service cloud.firestore {
2 | match /databases/{database}/documents {
3 |
4 | match /users/{userId} {
5 | allow read:
6 | if request.auth != null
7 | && request.auth.uid == userId;
8 | }
9 |
10 | match /projects/{projectId} {
11 | function existingData() {
12 | return resource.data;
13 | }
14 |
15 | function incomingData() {
16 | return request.resource.data;
17 | }
18 |
19 | function allowListProject() {
20 | return request.auth != null
21 | && existingData().users[request.auth.uid].email == request.auth.token.email;
22 | }
23 |
24 | function allowCreateProject() {
25 | return request.auth != null
26 | && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.permissions.createProject == true
27 | && incomingData().users[request.auth.uid].email == request.auth.token.email
28 | && incomingData().users[request.auth.uid].userExist == true
29 | && incomingData().users[request.auth.uid].permissions.updateDraft == true
30 | && incomingData().users[request.auth.uid].permissions.updateSchema == true
31 | && incomingData().users[request.auth.uid].permissions.updateSettings == true
32 | && incomingData().users[request.auth.uid].permissions.updateUsers == true
33 | && incomingData().users[request.auth.uid].permissions.publish == true
34 | && incomingData().users[request.auth.uid].permissions.createJob == true
35 | && projectId != 'index'
36 | && projectId != 'edit'
37 | && projectId != 'content'
38 | && projectId != 'schema'
39 | && projectId != 'settings'
40 | && projectId != 'project';
41 | }
42 |
43 | function allowUpdateDraft() {
44 | return request.auth != null
45 | && existingData().users[request.auth.uid].permissions.updateDraft == true
46 | && request.writeFields.size() == 1
47 | && request.writeFields[0].matches('^draft\\..*');
48 | }
49 |
50 | function allowUpdatePublished() {
51 | return request.auth != null
52 | && existingData().users[request.auth.uid].permissions.publish == true
53 | && request.writeFields.size() == 1
54 | && request.writeFields[0] == 'published';
55 | }
56 |
57 | function allowUpdateSchema() {
58 | return request.auth != null
59 | && existingData().users[request.auth.uid].permissions.updateSchema == true
60 | && request.writeFields.size() == 1
61 | && request.writeFields[0] == 'schema';
62 | }
63 |
64 | function allowUpdateSettings() {
65 | return request.auth != null
66 | && existingData().users[request.auth.uid].permissions.updateSettings == true
67 | && request.writeFields.size() == 1
68 | && request.writeFields[0].matches('^settings\\..*');
69 | }
70 |
71 | function allowUpdateUsers() {
72 | return request.auth != null
73 | && existingData().users[request.auth.uid].permissions.updateUsers == true
74 | && request.writeFields.size() == 1
75 | && request.writeFields[0].matches('^users\\..*');
76 | }
77 |
78 | allow list:
79 | if allowListProject();
80 |
81 | allow create:
82 | if allowCreateProject();
83 |
84 | allow update:
85 | if allowUpdateDraft()
86 | || allowUpdatePublished()
87 | || allowUpdateSchema()
88 | || allowUpdateSettings()
89 | || allowUpdateUsers();
90 | }
91 |
92 | match /projects/{projectId}/versions/{versionId} {
93 | allow create:
94 | if request.auth != null
95 | && get(/databases/$(database)/documents/projects/$(projectId)).data.users[request.auth.uid].permissions.publish == true;
96 | }
97 |
98 | match /projects/{projectId}/jobs/{jobId} {
99 | allow create:
100 | if request.auth != null
101 | && get(/databases/$(database)/documents/projects/$(projectId)).data.users[request.auth.uid].permissions.createJob == true;
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/rules/storage.rules:
--------------------------------------------------------------------------------
1 | service firebase.storage {
2 | match /b/{bucket}/o {
3 | match /{projectId}/{file} {
4 | allow write: if request.auth != null
5 | && resource == null
6 | && request.resource.size < 5 * 1024 * 1024
7 | && request.resource.contentType.matches('image/.*');
8 |
9 | allow read: if request.auth != null
10 | && resource.contentType.matches('image/.*');
11 | }
12 |
13 | // match /{projectId}/public/{file} {
14 | // allow read: if resource.contentType.matches('application/json; charset=utf-8');
15 | // }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/scripts/cors.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path')
3 | const execa = require('execa')
4 |
5 | ;(async () => {
6 | const firebasersPath = path.join(__dirname, '/../.firebaserc')
7 | const firebaserc = JSON.parse(fs.readFileSync(firebasersPath).toString())
8 | const aliases = Object.keys(firebaserc.projects)
9 | const arguments = process.argv.slice(2)
10 | const job = arguments[0]
11 | const alias = arguments[1]
12 | const envPath = path.join(__dirname, `/../.env.${alias}.local`)
13 |
14 | // Is gsutil installed
15 | try {
16 | execa.shellSync(`gsutil version`)
17 | }
18 | catch (error) {
19 | console.log(`
20 | You need to install gsutil
21 | https://cloud.google.com/storage/docs/gsutil_install
22 | `)
23 | return false
24 | }
25 |
26 | // Job is required
27 | if (!['allow', 'disallow', 'status'].includes(job)) {
28 | console.log(`
29 | Job "${job}" is not available. Use can use the following:
30 | - allow
31 | - disallow
32 | - status
33 | Example: npm run cors allow production
34 | `)
35 | return false
36 | }
37 |
38 | // Alias need to be in .firebaserc
39 | if (!aliases.includes(alias)) {
40 | console.log(`
41 | Alias "${alias}" is not available. Use can use the following:
42 | - ${aliases.join('\n - ')}
43 | Example: npm run deploy production
44 | `)
45 | return false
46 | }
47 |
48 | // Get storage bucket name
49 | const storageBucket = fs.readFileSync(envPath)
50 | .toString()
51 | .split('\n')
52 | .filter(line => {
53 | return line.startsWith('VUE_APP_FIREBASE_STORAGE_BUCKET=')
54 | })
55 | .toString()
56 | .replace('VUE_APP_FIREBASE_STORAGE_BUCKET=', '')
57 |
58 | // Run the job
59 | if (job === 'allow') {
60 | console.log(`Allow CORS on the alias ${alias}`)
61 | const rulesCorsPath = path.join(__dirname, `/../rules/cors.json`)
62 | execa.shellSync(`gsutil cors set ${rulesCorsPath} gs://${storageBucket}`, { stdio: 'inherit' })
63 | }
64 | else if (job === 'disallow') {
65 | console.log(`Disallow CORS on the alias ${alias}`)
66 | const tempPath = path.join(__dirname, `/tempDisallow.json`)
67 | fs.writeFileSync(tempPath, '[]', 'utf-8')
68 | execa.shellSync(`gsutil cors set ${tempPath} gs://${storageBucket}`, { stdio: 'inherit' })
69 | fs.unlinkSync(tempPath)
70 | }
71 | else if (job === 'status') {
72 | console.log(`Get CORS status from the alias ${alias}`)
73 | }
74 |
75 | execa.shellSync(`gsutil cors get gs://${storageBucket}`, { stdio: 'inherit' })
76 |
77 | })().catch(err => console.error(err))
78 |
--------------------------------------------------------------------------------
/scripts/deploy.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path')
3 | const execa = require('execa')
4 |
5 | const createEnvFile = (envPath, alias) => {
6 | const setupWeb = execa.shellSync(`firebase setup:web`)
7 | const envFileContent = setupWeb.stdout
8 | .toString()
9 | .split('\n')
10 | .map(line => {
11 | if (line.includes('": "')) {
12 | const pair = line.split('"')
13 | const key = pair[1]
14 | const value = pair[3]
15 |
16 | if (key === 'apiKey') {
17 | return `VUE_APP_FIREBASE_API_KEY=${value}`
18 | }
19 | else if (key === 'authDomain') {
20 | return `VUE_APP_FIREBASE_AUTH_DOMAIN=${value}`
21 | }
22 | else if (key === 'databaseURL') {
23 | return `VUE_APP_FIREBASE_DATABASE_URL=${value}`
24 | }
25 | else if (key === 'projectId') {
26 | return `VUE_APP_FIREBASE_PROJECT_ID=${value}`
27 | }
28 | else if (key === 'storageBucket') {
29 | return `VUE_APP_FIREBASE_STORAGE_BUCKET=${value}`
30 | }
31 | }
32 |
33 | return null
34 | })
35 | .filter(line => line)
36 | .join('\n')
37 |
38 | fs.writeFileSync(envPath, envFileContent, 'utf-8')
39 |
40 | console.log(`
41 | Created file .env.${alias}.local
42 | ${envFileContent}
43 | `)
44 | }
45 |
46 | ;(async () => {
47 | const firebasersPath = path.join(__dirname, '/../.firebaserc')
48 | const firebaserc = JSON.parse(fs.readFileSync(firebasersPath).toString())
49 | const aliases = Object.keys(firebaserc.projects)
50 | const arguments = process.argv.slice(2)
51 | const alias = arguments[0]
52 | const envPath = path.join(__dirname, `/../.env.${alias}.local`)
53 |
54 | // Is firebase-tools installed
55 | try {
56 | execa.shellSync(`npx firebase --version`)
57 | }
58 | catch (error) {
59 | console.log(`
60 | You need to Firebase Tools
61 | npm install -g firebase-tools
62 | https://firebase.google.com/docs/cli/
63 | `)
64 | return false
65 | }
66 |
67 | // Alias need to be in .firebaserc
68 | if (!aliases.includes(alias)) {
69 | console.log(`
70 | Alias "${alias}" is not available. Use can use the following:
71 | - ${aliases.join('\n - ')}
72 | Example: npm run deploy production
73 | `)
74 | return false
75 | }
76 |
77 | // Lint
78 | execa.shellSync(`npx vue-cli-service lint`, { stdio: 'inherit' })
79 | execa.shellSync(`npx eslint ./functions/src`, { stdio: 'inherit' })
80 |
81 | // Use selected alias
82 | execa.shellSync(`npx firebase use ${alias}`, { stdio: 'inherit' })
83 |
84 | // Create .env.[alias].local File if not exists or project id is changed
85 | if (!fs.existsSync(envPath)) {
86 | createEnvFile(envPath, alias)
87 | }
88 | else {
89 | const projectId = fs.readFileSync(envPath)
90 | .toString()
91 | .split('\n')
92 | .map(line => {
93 | const pair = line.split('=')
94 | const key = pair[0]
95 | const value = pair[1]
96 |
97 | if (key === 'VUE_APP_FIREBASE_PROJECT_ID') {
98 | return value
99 | }
100 |
101 | return null
102 | })
103 | .filter(line => line)
104 | .toString()
105 |
106 | if (firebaserc.projects[alias] !== projectId) {
107 | createEnvFile(envPath, alias)
108 | }
109 | }
110 |
111 | // Build functions
112 | execa.shellSync('npx babel functions/src --out-dir functions/dist', { stdio: 'inherit' })
113 |
114 | // Build app if not development
115 | if (alias !== 'development') {
116 | execa.shellSync(`npx vue-cli-service build --mode ${alias}`, { stdio: 'inherit' })
117 | }
118 |
119 | // Deploy to Firebase
120 | if (alias !== 'development') {
121 | execa.shellSync('npx firebase deploy', { stdio: 'inherit' })
122 | }
123 | else {
124 | execa.shellSync('npx firebase deploy --only functions,firestore,storage', { stdio: 'inherit' })
125 | }
126 |
127 | // Back to development env if exsist
128 | if (aliases.includes('development')) {
129 | execa.shellSync(`npx firebase use development`, { stdio: 'inherit' })
130 | }
131 |
132 | })().catch(err => console.error(err))
133 |
--------------------------------------------------------------------------------
/scripts/open.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path')
3 | const execa = require('execa')
4 | const open = require('open')
5 |
6 | ;(async () => {
7 | const firebasersPath = path.join(__dirname, '/../.firebaserc')
8 | const firebaserc = JSON.parse(fs.readFileSync(firebasersPath).toString())
9 | const aliases = Object.keys(firebaserc.projects)
10 | const arguments = process.argv.slice(2)
11 | const alias = arguments[0]
12 | const envPath = path.join(__dirname, `/../.env.${alias}.local`)
13 |
14 | // Alias need to be in .firebaserc
15 | if (!aliases.includes(alias)) {
16 | console.log(`
17 | Alias "${alias}" is not available. You can use the following:
18 | - ${aliases.join('\n - ')}
19 | Example: npm run deploy production
20 | `)
21 | return false
22 | }
23 |
24 | // Get url
25 | const url = fs.readFileSync(envPath)
26 | .toString()
27 | .split('\n')
28 | .filter(line => {
29 | return line.startsWith('VUE_APP_FIREBASE_AUTH_DOMAIN=')
30 | })
31 | .toString()
32 | .replace('VUE_APP_FIREBASE_AUTH_DOMAIN=', 'https://')
33 |
34 | if (alias !== 'development') {
35 | console.log(`Opening URL ${url} in the browser.`)
36 | open(url, {wait: false})
37 | }
38 | else {
39 | execa.shellSync(`npm run serve`, { stdio: 'inherit' })
40 | }
41 |
42 | })().catch(err => console.error(err))
43 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
101 |
102 |
103 |
104 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
174 |
--------------------------------------------------------------------------------
/src/assets/image-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/raneio/editlayer/9fe46e34377c13a6626780acc98971eb023eca5a/src/assets/image-background.png
--------------------------------------------------------------------------------
/src/components/content/Editor.vue:
--------------------------------------------------------------------------------
1 |
75 |
76 |
77 |
78 |
79 | {{activeItem.TITLE}}
80 |
81 |
82 |
83 | {{activeItem.INFO}}
84 |
85 |
86 |
87 |
88 |
89 | Editor "{{activeItem.EDITOR}}" is not supported, please change editor in the schema. Following editors are supported: {{editors.join(', ')}}
90 |
91 |
92 |
93 |
Invalid Content
94 |
95 |
96 |
99 |
100 |
101 |
102 |
103 |
138 |
--------------------------------------------------------------------------------
/src/components/content/PartnerInfo.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 | This editor is available only for official partners of Editlayer.
18 |
19 |
20 |
21 | Become a partner
22 |
23 |
24 |
25 |
26 |
34 |
--------------------------------------------------------------------------------
/src/components/content/PublishButton.vue:
--------------------------------------------------------------------------------
1 |
90 |
91 |
92 |
123 |
124 |
125 |
168 |
--------------------------------------------------------------------------------
/src/components/core/Alert.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
27 |
28 |
29 |
30 |
31 |
69 |
--------------------------------------------------------------------------------
/src/components/core/Button.vue:
--------------------------------------------------------------------------------
1 |
45 |
46 |
47 |
59 |
60 |
61 |
62 |
63 |
150 |
--------------------------------------------------------------------------------
/src/components/core/Card.vue:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
34 |
35 |
36 |
91 |
--------------------------------------------------------------------------------
/src/components/core/Heading.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
22 |
23 |
24 |
46 |
--------------------------------------------------------------------------------
/src/components/core/Notification.vue:
--------------------------------------------------------------------------------
1 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | {{link.text}}
96 |
97 |
98 |
99 |
100 |
101 |
102 |
148 |
--------------------------------------------------------------------------------
/src/components/core/registerAll.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Alert from '@/components/core/Alert'
3 | import Button from '@/components/core/Button'
4 | import Heading from '@/components/core/Heading'
5 | import Card from '@/components/core/Card'
6 | import Notification from '@/components/core/Notification'
7 |
8 | Vue.component('alert-core', Alert)
9 | Vue.component('button-core', Button)
10 | Vue.component('heading-core', Heading)
11 | Vue.component('card-core', Card)
12 | Vue.component('notification-core', Notification)
13 |
--------------------------------------------------------------------------------
/src/components/dashboard/Navigation.vue:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
44 |
45 |
46 |
89 |
--------------------------------------------------------------------------------
/src/components/dashboard/NewProjectButton.vue:
--------------------------------------------------------------------------------
1 |
33 |
34 |
35 |
36 |
41 |
42 | New Project
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
55 |
--------------------------------------------------------------------------------
/src/components/dashboard/NewProjectModal.vue:
--------------------------------------------------------------------------------
1 |
36 |
37 |
38 |
58 |
59 |
60 |
83 |
--------------------------------------------------------------------------------
/src/components/dashboard/Projects.vue:
--------------------------------------------------------------------------------
1 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | {{usersText(project, 'admin', 'Admin', 'Admins')}}
62 |
63 |
64 | ,
65 |
66 |
67 | {{usersText(project, 'editor', 'Editor', 'Editors')}}
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
127 |
--------------------------------------------------------------------------------
/src/components/navigation/BackButton.vue:
--------------------------------------------------------------------------------
1 |
109 |
110 |
111 |
112 |
113 | Dashboard
114 | Back
115 |
116 |
117 |
118 |
123 |
--------------------------------------------------------------------------------
/src/components/navigation/Breadcrumb.vue:
--------------------------------------------------------------------------------
1 |
104 |
105 |
106 |
107 |
108 |
113 |
114 |
115 |
116 |
121 | {{item.name}}
122 |
123 |
124 |
125 | {{item.name}}
126 |
127 |
128 |
129 |
130 |
131 |
132 |
156 |
--------------------------------------------------------------------------------
/src/components/navigation/Navigation.vue:
--------------------------------------------------------------------------------
1 |
33 |
34 |
35 |
84 |
85 |
86 |
142 |
--------------------------------------------------------------------------------
/src/components/panel/Item.vue:
--------------------------------------------------------------------------------
1 |
106 |
107 |
108 |
119 |
120 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
217 |
--------------------------------------------------------------------------------
/src/components/panel/ItemsFromObject.vue:
--------------------------------------------------------------------------------
1 |
92 |
93 |
94 |
95 |
96 |
99 |
100 |
101 |
102 |
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/src/components/panel/Panel.vue:
--------------------------------------------------------------------------------
1 |
76 |
77 |
78 |
84 |
85 |
86 |
122 |
--------------------------------------------------------------------------------
/src/components/schema/SchemaEditor.vue:
--------------------------------------------------------------------------------
1 |
102 |
103 |
104 |
105 |
106 |
107 | Invalid JSON Syntax
108 |
109 |
110 |
120 |
121 |
122 |
123 | Supported editors:
124 | {{editors.join(', ')}}
125 |
126 |
127 |
128 |
129 |
130 |
156 |
--------------------------------------------------------------------------------
/src/components/settings/DeleteProject.vue:
--------------------------------------------------------------------------------
1 |
37 |
38 |
39 |
40 |
41 | Delete project
42 | Your project will be deleted permanently and you can’t undo this.
43 |
44 |
45 |
46 | Delete Project Permamently
47 |
48 |
49 |
50 |
51 |
59 |
--------------------------------------------------------------------------------
/src/components/settings/FileLocation.vue:
--------------------------------------------------------------------------------
1 |
46 |
47 |
48 |
49 |
50 | JSON location
51 | You can find latest published JSON file from the following URL address
52 |
53 |
54 |
62 | {{activeProject.jsonUrl}}
63 |
64 |
65 |
79 |
80 |
81 | You haven't published content. Click the "Publish" button at the top-left corner.
82 |
83 |
84 |
85 |
86 |
87 |
110 |
--------------------------------------------------------------------------------
/src/components/settings/NewUserButton.vue:
--------------------------------------------------------------------------------
1 |
33 |
34 |
35 |
36 |
41 |
42 | New user
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
55 |
--------------------------------------------------------------------------------
/src/components/settings/NewUserModal.vue:
--------------------------------------------------------------------------------
1 |
68 |
69 |
70 |
94 |
95 |
96 |
122 |
--------------------------------------------------------------------------------
/src/components/settings/Users.vue:
--------------------------------------------------------------------------------
1 |
74 |
75 |
76 |
77 |
78 | Users
79 | You can add permissions to other users.
80 |
81 |
82 |
83 |
84 |
85 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
169 |
--------------------------------------------------------------------------------
/src/components/settings/Webhook.vue:
--------------------------------------------------------------------------------
1 |
128 |
129 |
130 |
131 |
132 | Webhook
133 | We will send a custom POST/GET request when publishing is done.
134 |
135 |
136 |
137 | Variable
{{BASE64_CONTENT}}
is published content encoded with
Base64.
138 | Variable {{VERSION_ID}}
is version of published JSON.
139 | Variable {{PUBLISHER_EMAIL}}
is email of publisher.
140 |
141 |
142 |
153 |
154 |
155 | Disable webhook
156 | Test webhook
157 |
158 | Open web console to debug
159 |
160 |
161 | Axios API
162 |
163 |
164 |
165 | Enable webhook
166 |
167 |
168 |
169 |
170 |
171 |
198 |
--------------------------------------------------------------------------------
/src/components/utils/FooterContent.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
19 |
20 |
21 |
68 |
--------------------------------------------------------------------------------
/src/components/utils/LoaderOverlay.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
Loading
12 |
13 |
14 |
15 |
16 |
47 |
--------------------------------------------------------------------------------
/src/components/utils/Notifications.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
47 |
--------------------------------------------------------------------------------
/src/editors/Checkbox.vue:
--------------------------------------------------------------------------------
1 |
58 |
59 |
60 |
77 |
78 |
79 |
93 |
--------------------------------------------------------------------------------
/src/editors/Code.vue:
--------------------------------------------------------------------------------
1 |
74 |
75 |
76 |
85 |
86 |
87 |
91 |
--------------------------------------------------------------------------------
/src/editors/Color.vue:
--------------------------------------------------------------------------------
1 |
22 |
23 |
95 |
96 |
97 |
98 |
99 |
102 |
103 |
108 |
109 |
110 |
111 |
112 |
157 |
--------------------------------------------------------------------------------
/src/editors/Image.vue:
--------------------------------------------------------------------------------
1 |
74 |
75 |
76 |
77 |
78 |
79 |
![]()
80 |
81 |
82 |
83 |
84 |
90 | Upload Image
91 |
92 |
93 |
94 |
95 |
103 |
104 |
105 |
106 |
107 |
133 |
--------------------------------------------------------------------------------
/src/editors/Markdown.vue:
--------------------------------------------------------------------------------
1 |
112 |
113 |
114 |
152 |
153 |
154 |
257 |
--------------------------------------------------------------------------------
/src/editors/Number.vue:
--------------------------------------------------------------------------------
1 |
39 |
40 |
41 |
52 |
53 |
54 |
58 |
--------------------------------------------------------------------------------
/src/editors/Radio.vue:
--------------------------------------------------------------------------------
1 |
38 |
39 |
40 |
57 |
58 |
59 |
73 |
--------------------------------------------------------------------------------
/src/editors/RichText.vue:
--------------------------------------------------------------------------------
1 |
134 |
135 |
136 |
166 |
167 |
168 |
248 |
--------------------------------------------------------------------------------
/src/editors/Select.vue:
--------------------------------------------------------------------------------
1 |
38 |
39 |
40 |
41 |
42 |
43 | OPTIONS is required with "select" editor. Fix your schema.
44 |
45 |
46 |
53 |
54 |
55 |
56 |
57 |
61 |
--------------------------------------------------------------------------------
/src/editors/SelectImage.vue:
--------------------------------------------------------------------------------
1 |
38 |
39 |
40 |
41 |
42 |
43 | OPTIONS is required with "select-image" editor. Fix your schema.
44 |
45 |
46 |
47 |
48 |
53 |
54 |
![]()
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
102 |
--------------------------------------------------------------------------------
/src/editors/Text.vue:
--------------------------------------------------------------------------------
1 |
36 |
37 |
38 |
47 |
48 |
49 |
53 |
--------------------------------------------------------------------------------
/src/editors/Textarea.vue:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 |
35 |
36 |
37 |
47 |
--------------------------------------------------------------------------------
/src/editors/Video.vue:
--------------------------------------------------------------------------------
1 |
126 |
127 |
128 |
140 |
141 |
142 |
165 |
--------------------------------------------------------------------------------
/src/editors/common/BaseEditor.vue:
--------------------------------------------------------------------------------
1 |
75 |
--------------------------------------------------------------------------------
/src/editors/common/BasePreview.vue:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/src/editors/config.js:
--------------------------------------------------------------------------------
1 | // Editors
2 | import Calendar from '@/editors/Calendar'
3 | import Checkbox from '@/editors/Checkbox'
4 | import Code from '@/editors/Code'
5 | import Color from '@/editors/Color'
6 | import Image from '@/editors/Image'
7 | // import Input from '@/editors/Input'
8 | import Markdown from '@/editors/Markdown'
9 | import Number from '@/editors/Number'
10 | import Radio from '@/editors/Radio'
11 | import RichText from '@/editors/RichText'
12 | import Select from '@/editors/Select'
13 | import SelectImage from '@/editors/SelectImage'
14 | import Text from '@/editors/Text'
15 | import Textarea from '@/editors/Textarea'
16 | import Video from '@/editors/Video'
17 |
18 | // Previews
19 | import ArrayPreview from '@/editors/previews/Array'
20 | import CalendarPreview from '@/editors/previews/Calendar'
21 | import CodePreview from '@/editors/previews/Code'
22 | import ColorPreview from '@/editors/previews/Color'
23 | import ImagePreview from '@/editors/previews/Image'
24 | import OptionPreview from '@/editors/previews/Option'
25 | import TextPreview from '@/editors/previews/Text'
26 | import VideoPreview from '@/editors/previews/Video'
27 |
28 | export default [
29 | {
30 | 'schemaName': 'calendar',
31 | 'editor': Calendar,
32 | 'preview': CalendarPreview,
33 | },
34 | {
35 | 'schemaName': 'checkbox',
36 | 'editor': Checkbox,
37 | 'preview': ArrayPreview,
38 | },
39 | {
40 | 'schemaName': 'code',
41 | 'editor': Code,
42 | 'preview': CodePreview,
43 | },
44 | {
45 | 'schemaName': 'color',
46 | 'editor': Color,
47 | 'preview': ColorPreview,
48 | },
49 | {
50 | 'schemaName': 'image',
51 | 'editor': Image,
52 | 'preview': ImagePreview,
53 | },
54 | // {
55 | // 'schemaName': 'input',
56 | // 'editor': Input,
57 | // 'preview': TextPreview,
58 | // },
59 | {
60 | 'schemaName': 'markdown',
61 | 'editor': Markdown,
62 | 'preview': TextPreview,
63 | },
64 | {
65 | 'schemaName': 'number',
66 | 'editor': Number,
67 | 'preview': TextPreview,
68 | },
69 | {
70 | 'schemaName': 'radio',
71 | 'editor': Radio,
72 | 'preview': OptionPreview,
73 | },
74 | {
75 | 'schemaName': 'rich-text',
76 | 'editor': RichText,
77 | 'preview': TextPreview,
78 | },
79 | {
80 | 'schemaName': 'select',
81 | 'editor': Select,
82 | 'preview': OptionPreview,
83 | },
84 | {
85 | 'schemaName': 'select-image',
86 | 'editor': SelectImage,
87 | 'preview': ImagePreview,
88 | },
89 | {
90 | 'schemaName': 'text',
91 | 'editor': Text,
92 | 'preview': TextPreview,
93 | },
94 | {
95 | 'schemaName': 'textarea',
96 | 'editor': Textarea,
97 | 'preview': TextPreview,
98 | },
99 | {
100 | 'schemaName': 'video',
101 | 'editor': Video,
102 | 'preview': VideoPreview,
103 | },
104 | ]
105 |
--------------------------------------------------------------------------------
/src/editors/previews/Array.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
25 |
26 |
27 |
47 |
--------------------------------------------------------------------------------
/src/editors/previews/Calendar.vue:
--------------------------------------------------------------------------------
1 |
46 |
47 |
48 |
62 |
63 |
64 |
88 |
--------------------------------------------------------------------------------
/src/editors/previews/Code.vue:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
25 |
26 |
27 |
41 |
--------------------------------------------------------------------------------
/src/editors/previews/Color.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
24 |
--------------------------------------------------------------------------------
/src/editors/previews/Image.vue:
--------------------------------------------------------------------------------
1 |
31 |
32 |
33 |
34 |
![]()
35 |
36 |
37 |
38 |
54 |
--------------------------------------------------------------------------------
/src/editors/previews/Option.vue:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 |
28 |
29 |
30 |
41 |
--------------------------------------------------------------------------------
/src/editors/previews/Text.vue:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 |
29 |
30 |
31 |
42 |
--------------------------------------------------------------------------------
/src/editors/previews/Video.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
![]()
24 |
25 |
26 |
27 |
28 |
56 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import { sync } from 'vuex-router-sync'
3 | import App from '@/App.vue'
4 | import router from '@/router'
5 | import store from '@/store'
6 |
7 | // Open Sans font
8 | import 'typeface-open-sans'
9 |
10 | // Font awesome icons
11 | import '@/utils/icons'
12 |
13 | // Core components and style
14 | import '@/components/core/registerAll'
15 | import '@/sass/core/initialize.sass'
16 |
17 | // Vue Transitions
18 | import '@/sass/transitions.sass'
19 |
20 | // Sync router and store
21 | sync(store, router)
22 |
23 | new Vue({
24 | router,
25 | store,
26 | render: h => h(App),
27 | }).$mount('#app')
28 |
--------------------------------------------------------------------------------
/src/router.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 | import Content from '@/views/Content.vue'
4 | import Schema from '@/views/Schema.vue'
5 | import Settings from '@/views/Settings.vue'
6 |
7 | Vue.use(Router)
8 |
9 | export default new Router({
10 | mode: 'history',
11 | base: __dirname,
12 | routes: [
13 | {
14 | name: 'Dashboard',
15 | path: '/',
16 | },
17 | {
18 | name: 'Register',
19 | path: '/register',
20 | },
21 | {
22 | name: 'Schema',
23 | path: '/:projectId/schema/:path?',
24 | component: Schema,
25 | },
26 | {
27 | name: 'Settings',
28 | path: '/:projectId/settings/:path?',
29 | component: Settings,
30 | },
31 | {
32 | name: 'Content',
33 | path: '/:projectId/:path?',
34 | component: Content,
35 | },
36 | ],
37 | })
38 |
--------------------------------------------------------------------------------
/src/sass/codemirror.sass:
--------------------------------------------------------------------------------
1 | @import "variables"
2 |
3 | .vue-codemirror
4 | padding: $radius--large
5 | border-radius: $radius--large
6 |
7 | &.-dracula
8 | font-size: .9rem
9 | background-color: #282A35
10 | transition: box-shadow .2s
11 | box-shadow: $shadow--large
12 |
13 | .CodeMirror
14 | height: auto
15 |
--------------------------------------------------------------------------------
/src/sass/core/_breakpoint.sass:
--------------------------------------------------------------------------------
1 | //
2 | // Breakpoints
3 | // https://medium.freecodecamp.org/the-100-correct-way-to-do-css-breakpoints-88d6a5ba1862
4 | // --------------------------------------------------
5 | // .my-element
6 | // padding: 10px
7 | // +breakpoint('large')
8 | // padding: 20px
9 | // +breakpoint(1300px)
10 | // padding: 25px
11 |
12 | @mixin breakpoint($min-width: 0)
13 |
14 | @if $min-width == 'small'
15 | $min-width: $breakpoint--small
16 |
17 | @if $min-width == 'medium'
18 | $min-width: $breakpoint--medium
19 |
20 | @if $min-width == 'large'
21 | $min-width: $breakpoint--large
22 |
23 | @if $min-width == 'huge'
24 | $min-width: $breakpoint--huge
25 |
26 | @media (min-width: $min-width)
27 | @content
28 |
--------------------------------------------------------------------------------
/src/sass/core/_center.sass:
--------------------------------------------------------------------------------
1 | //
2 | // Center
3 | // --------------------------------------------------
4 | // .my-element
5 | // +center()
6 |
7 | @mixin center()
8 | display: flex
9 | justify-content: center
10 | align-items: center
11 |
--------------------------------------------------------------------------------
/src/sass/core/_chain.sass:
--------------------------------------------------------------------------------
1 | //
2 | // Chain
3 | // --------------------------------------------------
4 | // .my-element
5 | // +chain(1rem)
6 |
7 | @mixin chain($gap: null)
8 | display: flex
9 | flex-direction: row
10 | align-items: center
11 | justify-content: flex-start
12 |
13 | > *
14 | flex-shrink: 0
15 | flex-grow: 0
16 |
17 | hr
18 | flex-shrink: 1
19 | flex-grow: 1
20 |
21 | @if $gap != null
22 | > * + *
23 | margin-left: $gap
24 |
--------------------------------------------------------------------------------
/src/sass/core/_container.sass:
--------------------------------------------------------------------------------
1 | //
2 | // Container
3 | // --------------------------------------------------
4 | // .my-element
5 | // +container(['default', 'fluid', '300px'], 'left')
6 |
7 | @mixin container($width: 'default', $position: $container-position)
8 | $padding: $rem
9 | padding-top: $container-vertical-padding
10 | padding-bottom: $container-vertical-padding
11 |
12 | // Set width by name
13 | @if $width == 'default'
14 | $width: $container-max-width
15 |
16 | @if $width == 'small'
17 | $width: $breakpoint--small
18 |
19 | @if $width == 'medium'
20 | $width: $breakpoint--medium
21 |
22 | @if $width == 'large'
23 | $width: $breakpoint--large
24 |
25 | @if $width == 'huge'
26 | $width: $breakpoint--huge
27 |
28 | // Horizontal padding
29 | @if $width == 'fluid' or type-of($width) == number
30 | padding-left: $padding
31 | padding-right: $padding
32 |
33 | +breakpoint('small')
34 | $padding: $breakpoint--small * .05
35 | padding-left: $padding
36 | padding-right: $padding
37 |
38 | +breakpoint('medium')
39 | $padding: $breakpoint--medium * .05
40 | padding-left: $padding
41 | padding-right: $padding
42 |
43 | +breakpoint('large')
44 | $padding: $breakpoint--large * .05
45 | padding-left: $padding
46 | padding-right: $padding
47 |
48 | +breakpoint('huge')
49 | $padding: $breakpoint--huge * .05
50 | padding-left: $padding
51 | padding-right: $padding
52 |
53 | // Horizontal padding when browser is wider then container
54 | @if type-of($width) == number
55 |
56 | $horizontal-padding-when-wider-than-container: $rem
57 |
58 | @if $width >= $breakpoint--small
59 | $horizontal-padding-when-wider-than-container: $breakpoint--small * .05
60 |
61 | @if $width >= $breakpoint--medium
62 | $horizontal-padding-when-wider-than-container: $breakpoint--medium * .05
63 |
64 | @if $width >= $breakpoint--large
65 | $horizontal-padding-when-wider-than-container: $breakpoint--large * .05
66 |
67 | @if $width >= $breakpoint--huge
68 | $horizontal-padding-when-wider-than-container: $breakpoint--huge * .05
69 |
70 | @media (min-width: #{$width + $horizontal-padding-when-wider-than-container * 2})
71 |
72 | @if $position == 'center'
73 | padding-left: calc((100% - #{$width}) / 2)
74 | padding-right: calc((100% - #{$width}) / 2)
75 |
76 | @if $position == 'left'
77 | padding-right: calc(100% - #{$width} - #{$padding})
78 |
79 | @if $position == 'right'
80 | padding-left: calc(100% - #{$width} - #{$padding})
81 |
--------------------------------------------------------------------------------
/src/sass/core/_gap.sass:
--------------------------------------------------------------------------------
1 | //
2 | // Gap between child-elements
3 | // --------------------------------------------------
4 | // .my-element
5 | // +gap(1rem)
6 |
7 | @mixin gap($margin: 1rem)
8 | > * + *
9 | margin-top: $margin
10 |
--------------------------------------------------------------------------------
/src/sass/core/_grid.sass:
--------------------------------------------------------------------------------
1 | //
2 | // Grid
3 | // --------------------------------------------------
4 | // .my-element
5 | // +grid(3, 2rem, 1rem)
6 | //
7 | // Param $columns [amount of columns (1-99)]
8 |
9 | @mixin grid($columns: 3, $horizontal-gap: null, $vertical-gap: null)
10 |
11 | @if $vertical-gap == null
12 | $vertical-gap: $horizontal-gap
13 |
14 | @if type-of($columns) == number and unitless($columns)
15 | display: flex
16 | flex-wrap: wrap
17 |
18 | > *
19 | width: 100% / $columns
20 |
21 | @if $horizontal-gap != null
22 | margin-left: $horizontal-gap * -1
23 |
24 | > *
25 | padding-left: $horizontal-gap
26 |
27 | @if $vertical-gap != null
28 | margin-bottom: $vertical-gap * -1
29 |
30 | > *
31 | padding-bottom: $vertical-gap
32 |
--------------------------------------------------------------------------------
/src/sass/core/_hx.sass:
--------------------------------------------------------------------------------
1 | //
2 | // H1-H6 shorthand
3 | // --------------------------------------------------
4 | // +hx
5 | // color: red
6 |
7 | @mixin hx()
8 | h1,
9 | h2,
10 | h3,
11 | h4,
12 | h5,
13 | h6
14 | @content
15 |
--------------------------------------------------------------------------------
/src/sass/core/_invert.sass:
--------------------------------------------------------------------------------
1 | //
2 | // Invert colors
3 | // --------------------------------------------------
4 | // .my-element
5 | // +invert
6 |
7 | @mixin invert()
8 | background-color: $invert-background-color
9 | color: $invert-content-color
10 | font-weight: $invert-content-font-weight
11 |
12 | a
13 | color: $invert-link-color
14 |
15 | &:hover
16 | color: $invert-link-color--hover
17 |
18 | &:active
19 | color: $invert-link-color--active
20 |
21 | +hx
22 | color: $invert-heading-color
23 | font-weight: $invert-heading-font-weight
24 |
25 | hr
26 | border-color: $invert-hr-color
27 |
--------------------------------------------------------------------------------
/src/sass/core/_mask.sass:
--------------------------------------------------------------------------------
1 | //
2 | // Text Masking
3 | // http://nimbupani.com/using-background-clip-for-text-with-css-fallback.html
4 | // --------------------------------------------------
5 | // .my-element
6 | // +mask('images/foobar.jpg')
7 |
8 | @mixin mask($image-url: null)
9 | background-size: cover
10 | background-position: center
11 | -webkit-background-clip: text
12 | -webkit-text-fill-color: transparent
13 |
14 | @if $image-url != null
15 | background: -webkit-linear-gradient(transparent, transparent), url($image-url)
16 |
--------------------------------------------------------------------------------
/src/sass/core/mixins.sass:
--------------------------------------------------------------------------------
1 | @import "breakpoint"
2 | @import "center"
3 | @import "chain"
4 | @import "container"
5 | @import "grid"
6 | @import "hx"
7 | @import "invert"
8 | @import "mask"
9 | @import "gap"
10 |
--------------------------------------------------------------------------------
/src/sass/transitions.sass:
--------------------------------------------------------------------------------
1 | @import "variables"
2 |
3 | .fade-enter-active,
4 | .fade-leave-active,
5 | transition: opacity $time
6 |
7 | .fade--fast-enter-active,
8 | .fade--fast-leave-active
9 | transition: opacity $time--small
10 |
11 | .fade--slow-enter-active,
12 | .fade--slow-leave-active
13 | transition: opacity $time--large
14 |
15 | .fade-enter,
16 | .fade-leave-to,
17 | .fade--fast-enter,
18 | .fade--fast-leave-to,
19 | .fade--slow-enter,
20 | .fade--slow-leave-to
21 | opacity: 0
22 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import _ from 'lodash'
4 | import parseJson from 'parse-json'
5 | import buildStructure from '@/utils/buildStructure'
6 | import firestore from '@/store/modules/firestore'
7 | import notifications from '@/store/modules/notifications'
8 | import projects from '@/store/modules/projects'
9 | import uploader from '@/store/modules/uploader'
10 | import utils from '@/store/modules/utils'
11 |
12 | Vue.use(Vuex)
13 |
14 | let store = {
15 | strict: true,
16 | modules: {
17 | firestore,
18 | notifications,
19 | projects,
20 | uploader,
21 | utils,
22 | },
23 |
24 | getters: {
25 |
26 | isLoggedIn (state) {
27 | if (state.firestore.user === null) {
28 | return null
29 | }
30 | else if (state.firestore.user === false || !state.firestore.user.email) {
31 | return false
32 | }
33 | else {
34 | return true
35 | }
36 | },
37 |
38 | auth (state) {
39 | return state.firestore.user
40 | },
41 |
42 | structure (state, getters) {
43 | if (!getters.activeProject) return {}
44 |
45 | const schema = parseJson(getters.activeProject.schema)
46 | const draft = getters.activeProject.draft
47 | const published = _.has(getters.activeProject, 'publishedVersion.draft') ? getters.activeProject.publishedVersion.draft : {}
48 |
49 | return buildStructure(schema, draft, published)
50 | },
51 |
52 | activeItem (state, getters, rootState) {
53 | if (!getters.activeProject) return {}
54 |
55 | let path = _.replace(rootState.route.params.path, />/g, '.')
56 |
57 | if (_.has(getters.structure, path)) {
58 | return _.get(getters.structure, path)
59 | }
60 | else {
61 | return {}
62 | }
63 | },
64 | },
65 |
66 | actions: {
67 | updateContent ({ getters, dispatch }, payload) {
68 | let currentContent = _.get(getters.structure, `${payload.path}._content`)
69 |
70 | if (!_.isEqual(currentContent, payload.content)) {
71 | dispatch('updateDraftToFirestore', payload)
72 | }
73 | },
74 |
75 | updateSchema ({ dispatch }, payload) {
76 | dispatch('updateSchemaToFirestore', payload)
77 | },
78 | },
79 |
80 | }
81 |
82 | export default new Vuex.Store(store)
83 |
--------------------------------------------------------------------------------
/src/store/modules/auth.js:
--------------------------------------------------------------------------------
1 | // import Vue from 'vue'
2 | // import _ from 'lodash'
3 | // import router from '@/router'
4 | // import firebase from '@/utils/firebase'
5 | //
6 | // export default {
7 | // state: {
8 | // id: null,
9 | // email: null,
10 | // },
11 | //
12 | // getters: {
13 | //
14 | // userSettings (rootState) {
15 | // return _.get(rootState, 'firestoreuserSettings')
16 | // },
17 | //
18 | // },
19 | //
20 | // mutations: {
21 | // setUser (state, user) {
22 | // Vue.set(state, 'id', user.id)
23 | // Vue.set(state, 'email', user.email)
24 | // },
25 | // },
26 | //
27 | // actions: {
28 | // authState ({state, commit, dispatch, rootState}) {
29 | // firebase.auth.onAuthStateChanged(firebaseUser => {
30 | // if (firebaseUser) {
31 | // commit('setUser', {
32 | // id: firebaseUser.uid,
33 | // email: firebaseUser.email,
34 | // })
35 | //
36 | // dispatch('getUserFromFirestore', firebaseUser.uid)
37 | //
38 | // dispatch('getProjectsFromFirestore', {
39 | // id: firebaseUser.uid,
40 | // email: firebaseUser.email,
41 | // })
42 | //
43 | // if (rootState.route.name === 'Register') {
44 | // router.push({name: 'Dashboard'})
45 | // }
46 | // }
47 | // else {
48 | // commit('setUser', {
49 | // id: false,
50 | // email: false,
51 | // })
52 | // }
53 | // })
54 | // },
55 | // },
56 | // }
57 |
--------------------------------------------------------------------------------
/src/store/modules/content.js:
--------------------------------------------------------------------------------
1 | // import _ from 'lodash'
2 | //
3 | // export default {
4 | //
5 | // actions: {
6 | // updateContent ({getters, dispatch}, payload) {
7 | // let currentContent = _.get(getters.structure, `${payload.path}._content`)
8 | //
9 | // if (!_.isEqual(currentContent, payload.content)) {
10 | // dispatch('updateDraftToFirestore', payload)
11 | // }
12 | // },
13 | // },
14 | // }
15 |
--------------------------------------------------------------------------------
/src/store/modules/firestore.js:
--------------------------------------------------------------------------------
1 | // import Vue from 'vue'
2 | // import dayjs from 'dayjs'
3 | import generate from 'nanoid/generate'
4 | import router from '@/router'
5 | import firebase from '@/utils/firebase'
6 |
7 | export default {
8 |
9 | state: {
10 | projects: null,
11 | user: null,
12 | },
13 |
14 | mutations: {
15 |
16 | setUser (state, user) {
17 | state.user = user
18 | },
19 |
20 | setProjects (state, projects) {
21 | state.projects = projects
22 | },
23 |
24 | },
25 |
26 | actions: {
27 | authState ({ state, commit, dispatch, rootState }) {
28 | firebase.auth.onAuthStateChanged(firebaseUser => {
29 | if (!firebaseUser) {
30 | commit('setUser', false)
31 | commit('setProjects', false)
32 | return null
33 | }
34 |
35 | dispatch('getUserFromFirestore', firebaseUser.uid)
36 | dispatch('getProjectsFromFirestore', {
37 | id: firebaseUser.uid,
38 | email: firebaseUser.email,
39 | })
40 |
41 | if (rootState.route.name === 'Register') {
42 | router.push({ name: 'Dashboard' })
43 | }
44 | })
45 | },
46 |
47 | getUserFromFirestore ({ state, commit }, userId) {
48 | firebase.firestore
49 | .collection('users')
50 | .doc(userId)
51 | .onSnapshot(docSnapshot => {
52 | commit('setUser', {
53 | id: userId,
54 | ...docSnapshot.data(),
55 | })
56 | })
57 | },
58 |
59 | getProjectsFromFirestore ({ state, commit }, payload) {
60 | firebase.firestore
61 | .collection('projects')
62 | .where(`users.${payload.id}.email`, '==', payload.email)
63 | .onSnapshot(querySnapshot => {
64 | let projects = {}
65 | querySnapshot.forEach(doc => {
66 | projects[doc.id] = doc.data()
67 | })
68 |
69 | commit('setProjects', projects)
70 | })
71 | },
72 |
73 | newProjectToFirestore ({ state, dispatch }, payload) {
74 | // console.log('newProjectToFirestore', payload)
75 | let newProject = payload.newProject
76 | payload = payload.payload
77 |
78 | firebase.firestore
79 | .collection('projects')
80 | .doc(payload.id)
81 | .set(newProject)
82 | .catch(error => {
83 | payload.tries = !payload.tries ? 1 : payload.tries + 1
84 |
85 | if (payload.tries < 5) {
86 | const orginalId = payload.tries > 1 ? payload.id.slice(0, -5) : payload.id
87 | const randomId = generate('abcdefghijklmnopqrstuvwxyz', 4)
88 | payload.id = `${orginalId}-${randomId}`
89 | dispatch('newProject', payload)
90 | }
91 | else {
92 | console.error('Creating new project failed', error)
93 | }
94 | })
95 | },
96 |
97 | deleteProjectFromFirestore ({ state }, payload) {
98 | firebase.firestore
99 | .collection('projects')
100 | .doc(payload.id)
101 | .collection('jobs')
102 | .add({
103 | job: 'deleteProject',
104 | deleteProjectId: payload.id,
105 | })
106 | .catch((error) => console.error('Project deleting failed', error))
107 | },
108 |
109 | newProjectVersionToFirestore ({ state }, payload) {
110 | return firebase.firestore
111 | .collection('projects')
112 | .doc(payload.projectId)
113 | .collection('versions')
114 | .add({
115 | publishedBy: payload.publishedBy,
116 | // publishedAt: null,
117 | publishedAt: firebase.firestoreTimestamp,
118 | json: payload.json,
119 | // filename: payload.filename,
120 | token: payload.token,
121 | schema: payload.schema,
122 | draft: payload.draft,
123 | })
124 | .catch((error) => console.error('Error adding version:', error))
125 | },
126 |
127 | setProjectPublished ({ state }, payload) {
128 | let updateData = {}
129 | updateData['published'] = {
130 | draft: payload.draft,
131 | schema: payload.schema,
132 | publishedAt: firebase.firestoreTimestamp,
133 | versionId: payload.versionId,
134 | }
135 |
136 | firebase.firestore
137 | .collection('projects')
138 | .doc(payload.projectId)
139 | .update(updateData)
140 | },
141 |
142 | updateDraftToFirestore ({ state, getters }, payload) {
143 | let updateContent = {}
144 | updateContent[`draft.${payload.path}`] = payload.content
145 |
146 | firebase.firestore
147 | .collection('projects')
148 | .doc(payload.projectId)
149 | .update(updateContent)
150 | .catch((error) => console.error('Error updating content:', error))
151 | },
152 |
153 | updateSchemaToFirestore ({ state }, payload) {
154 | firebase.firestore
155 | .collection('projects')
156 | .doc(payload.projectId)
157 | .update({
158 | schema: payload.schema,
159 | })
160 | .catch((error) => console.error('Error writing schema:', error))
161 | },
162 |
163 | newPermissionToFirestore ({ commit, getters }, payload) {
164 | firebase.firestore
165 | .collection('projects')
166 | .doc(payload.projectId)
167 | .collection('jobs')
168 | .add({
169 | job: 'addUserToProject',
170 | email: payload.email,
171 | permissions: {
172 | publish: true,
173 | updateDraft: true,
174 | },
175 | })
176 | .catch(error => console.error('Add user job adding failed', error))
177 | },
178 |
179 | async addUserToProjectToFirestore ({ commit, getters }, payload) {
180 | let userData = {}
181 | userData[`users.${payload.awaitId}`] = payload.user
182 |
183 | // console.log('addUserToProjectToFirestore', payload)
184 |
185 | await firebase.firestore
186 | .collection('projects')
187 | .doc(payload.projectId)
188 | .update(userData)
189 | .catch(error => console.error('Add user job adding failed', error))
190 |
191 | firebase.firestore
192 | .collection('projects')
193 | .doc(payload.projectId)
194 | .collection('jobs')
195 | .add({
196 | job: 'attachUserToProject',
197 | email: payload.email,
198 | awaitId: payload.awaitId,
199 | projectId: payload.projectId,
200 | })
201 | .catch(error => console.error('Add user job adding failed', error))
202 | },
203 |
204 | updatePermissionToFirestore ({ state }, payload) {
205 | // console.log('updatePermissionToFirestore', payload)
206 | let newRole = {}
207 | newRole[`users.${payload.userId}.permissions`] = payload.permissions
208 |
209 | firebase.firestore
210 | .collection('projects')
211 | .doc(payload.projectId)
212 | .update(newRole)
213 | .catch((error) => console.error('Permission updating failed', error))
214 | },
215 |
216 | removeUserFromProjectToFirestore ({ state }, payload) {
217 | // console.log('removeUserFromProjectToFirestore', payload)
218 | let removeUser = {}
219 | removeUser[`users.${payload.userId}`] = firebase.firestoreDelete
220 |
221 | // console.log('removeUser', removeUser)
222 |
223 | firebase.firestore
224 | .collection('projects')
225 | .doc(payload.projectId)
226 | .update(removeUser)
227 | .catch(error => console.error('Permission deleting failed', error))
228 | },
229 |
230 | },
231 |
232 | }
233 |
--------------------------------------------------------------------------------
/src/store/modules/notifications.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import _ from 'lodash'
3 | import nanoid from 'nanoid'
4 |
5 | export default {
6 |
7 | state: {
8 | items: {},
9 | },
10 |
11 | mutations: {
12 |
13 | setNotification (state, payload) {
14 | let notification = {}
15 | payload.id = payload.id || nanoid()
16 |
17 | if (_.has(state.items, payload.id)) {
18 | notification = _.get(state.items, payload.id)
19 | }
20 |
21 | notification.mode = payload.mode || notification.mode
22 | notification.message = payload.message || notification.message
23 | notification.link = payload.link || notification.link
24 | notification.progress = payload.progress || notification.progress
25 |
26 | Vue.set(state.items, payload.id, notification)
27 | },
28 |
29 | deleteNotification (state, payload) {
30 | Vue.delete(state.items, payload.id)
31 | },
32 |
33 | },
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/src/store/modules/schema.js:
--------------------------------------------------------------------------------
1 | // export default {
2 | //
3 | // actions: {
4 | //
5 | // updateSchema ({dispatch}, payload) {
6 | // dispatch('updateSchemaToFirestore', payload)
7 | // },
8 | //
9 | // },
10 | // }
11 |
--------------------------------------------------------------------------------
/src/store/modules/structure.js:
--------------------------------------------------------------------------------
1 | // import parseJson from 'parse-json'
2 | // import _ from 'lodash'
3 | // import buildStructure from '@/utils/buildStructure'
4 | //
5 | // export default {
6 | //
7 | // getters: {
8 | //
9 | // structure (state, getters) {
10 | // if (!getters.activeProject) return {}
11 | //
12 | // const schema = parseJson(getters.activeProject.schema)
13 | // const draft = getters.activeProject.draft
14 | // const published = _.has(getters.activeProject, 'publishedVersion.draft') ? getters.activeProject.publishedVersion.draft : {}
15 | //
16 | // return buildStructure(schema, draft, published)
17 | // },
18 | //
19 | // activeItem (state, getters, rootState) {
20 | // if (!getters.activeProject) return {}
21 | //
22 | // let path = _.replace(rootState.route.params.path, />/g, '.')
23 | //
24 | // if (_.has(getters.structure, path)) {
25 | // return _.get(getters.structure, path)
26 | // }
27 | // else {
28 | // return {}
29 | // }
30 | // },
31 | // },
32 | // }
33 |
--------------------------------------------------------------------------------
/src/store/modules/uploader.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import slugg from 'slugg'
3 | import generate from 'nanoid/generate'
4 | import firebase from '@/utils/firebase'
5 | import { blobToDataURL } from 'blob-util'
6 | import ImageCompressor from 'image-compressor.js'
7 |
8 | const imageCompressor = new ImageCompressor()
9 |
10 | export default {
11 |
12 | actions: {
13 |
14 | // TODO: Move this function to utils folder
15 | async uploadImage ({ commit, dispatch }, payload) {
16 | if (!_.startsWith(payload.image.type, 'image/')) return false
17 | let filenameWithoutExt = slugg(payload.image.name.replace(/\.[^/.]+$/, ''))
18 | let randomId = generate('abcdefghijklmnopqrstuvwxyz', 4)
19 |
20 | let maxWidth = 960
21 | let maxHeight = 960
22 |
23 | if (_.isNumber(payload.downscale)) {
24 | maxWidth = payload.downscale
25 | maxHeight = payload.downscale
26 | }
27 |
28 | if (_.isPlainObject(payload.downscale) && _.isNumber(payload.downscale.width)) {
29 | maxWidth = payload.downscale.width
30 | }
31 |
32 | if (_.isPlainObject(payload.downscale) && _.isNumber(payload.downscale.height)) {
33 | maxHeight = payload.downscale.height
34 | }
35 |
36 | commit('setNotification', {
37 | id: `${payload.projectId}>${payload.path}>upload`,
38 | mode: 'info',
39 | message: filenameWithoutExt,
40 | })
41 |
42 | let uploadImage = payload.image
43 |
44 | if (_.includes(['image/jpeg', 'image/png'], payload.image.type)) {
45 | let optimizedImage = await imageCompressor.compress(payload.image, {
46 | quality: 0.8,
47 | convertSize: 1000000,
48 | maxWidth: maxWidth,
49 | maxHeight: maxHeight,
50 | })
51 | .then((result) => {
52 | return result
53 | })
54 | .catch((error) => console.error('Image optimize failed', error.message))
55 |
56 | uploadImage = optimizedImage
57 | }
58 |
59 | if (uploadImage.size > 5 * 1024 * 1024) {
60 | commit('setNotification', {
61 | id: `${payload.projectId}>${payload.path}>upload`,
62 | mode: 'danger',
63 | message: 'Max image size 5 MB, try another image.',
64 | })
65 |
66 | console.error('Max image size 5 MB, try another image.')
67 | return false
68 | }
69 |
70 | let filename = `${filenameWithoutExt}-${randomId}`
71 |
72 | if (uploadImage.type === 'image/jpeg') {
73 | filename = `${filename}.jpg`
74 | }
75 | else if (uploadImage.type === 'image/png') {
76 | filename = `${filename}.png`
77 | }
78 | else if (uploadImage.type === 'image/gif') {
79 | filename = `${filename}.gif`
80 | }
81 | else if (uploadImage.type === 'image/svg+xml') {
82 | filename = `${filename}.svg`
83 | }
84 | else {
85 | commit('setNotification', {
86 | id: `${payload.projectId}>${payload.path}>upload`,
87 | mode: 'danger',
88 | message: `Unsupported file type. Send jpg, png, gif or svg.`,
89 | })
90 | console.error('Unsupported file type', uploadImage.type)
91 | return false
92 | }
93 |
94 | // TODO: Move this to storage.js
95 | let uploadTask = firebase.storage
96 | .ref()
97 | .child(`${payload.projectId}/${filename}`)
98 | .put(uploadImage)
99 |
100 | uploadTask.on('state_changed', (snapshot) => {
101 | commit('setNotification', {
102 | id: `${payload.projectId}>${payload.path}>upload`,
103 | progress: snapshot.bytesTransferred / snapshot.totalBytes * 100,
104 | })
105 | })
106 |
107 | let downloadURL = await uploadTask
108 | .then((data) => {
109 | return uploadTask
110 | .snapshot
111 | .ref
112 | .getDownloadURL()
113 | .then(downloadURL => downloadURL)
114 | })
115 | .catch((error) => console.error('Upload task faild', error))
116 |
117 | let thumbnail = null
118 |
119 | if (payload.projectId && payload.path && downloadURL) {
120 | if (_.includes(['image/jpeg', 'image/png', 'image/gif'], payload.image.type)) {
121 | const thumbnailBlob = await imageCompressor.compress(payload.image, {
122 | quality: 0.2,
123 | convertSize: 0,
124 | maxWidth: 64,
125 | maxHeight: 64,
126 | })
127 | .catch((error) => console.error('Image optimize failed', error.message))
128 |
129 | thumbnail = await blobToDataURL(thumbnailBlob)
130 | .catch((error) => console.error('Blob to base64 failed', error))
131 | }
132 |
133 | let img = new Image()
134 | img.src = URL.createObjectURL(uploadImage)
135 | img.onload = () => {
136 | dispatch('updateContent', {
137 | projectId: payload.projectId,
138 | path: payload.path,
139 | content: {
140 | url: downloadURL,
141 | height: img.height,
142 | width: img.width,
143 | size: uploadImage.size,
144 | type: uploadImage.type,
145 | thumbnail: thumbnail,
146 | },
147 | })
148 | }
149 | }
150 |
151 | return {
152 | downloadURL: downloadURL,
153 | filename: filenameWithoutExt,
154 | projectId: payload.projectId,
155 | path: payload.path,
156 | size: uploadImage.size,
157 | type: uploadImage.type,
158 | thumbnail: thumbnail,
159 | }
160 | },
161 | },
162 | }
163 |
--------------------------------------------------------------------------------
/src/store/modules/utils.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import _ from 'lodash'
3 |
4 | export default {
5 |
6 | state: {
7 | activeModal: null,
8 | editorContentIsValid: true,
9 | windowHeight: window.innerHeight,
10 | windowWidth: window.innerWidth,
11 | invalidSchemas: {},
12 | editlayerUrl: 'https://alpha5.editlayer.org',
13 | },
14 |
15 | mutations: {
16 |
17 | setEditorContentValid (state, boolean) {
18 | Vue.set(state, 'editorContentIsValid', boolean)
19 | },
20 |
21 | setwWindowSize (state) {
22 | Vue.set(state, 'windowWidth', window.innerWidth)
23 | Vue.set(state, 'windowHeight', window.innerHeight)
24 | },
25 |
26 | setActiveModal (state, modal) {
27 | Vue.set(state, 'activeModal', modal)
28 | },
29 |
30 | setInvalidSchema (state, payload) {
31 | state.invalidSchemas[payload.projectId] = payload.schema
32 | // Vue.set(state, 'invalidSchemas', payload.schema)
33 | },
34 |
35 | removeInvalidSchema (state, projectId) {
36 | let invalidSchemas = state.invalidSchemas
37 | delete invalidSchemas[projectId]
38 | state.invalidSchemas = invalidSchemas
39 | },
40 |
41 | },
42 |
43 | actions: {
44 |
45 | resizeListener ({ commit }) {
46 | window.addEventListener('resize', _.debounce(function () {
47 | commit('setwWindowSize')
48 | }, 250))
49 | },
50 |
51 | },
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/src/utils/buildJson.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 |
3 | const basicToJson = (schema, result = {}) => {
4 | _.each(schema, (value, key) => {
5 | if (_.has(value, '_content') && _.has(value, '_path')) {
6 | _.set(result, value._path, value._content)
7 | }
8 |
9 | if (_.has(value, '_order') && _.has(value, '_path')) {
10 | _.set(result, `${value._path}._order`, value._order)
11 | _.set(result, `${value._path}._key`, key)
12 | }
13 |
14 | if (_.isPlainObject(value)) {
15 | basicToJson(value, result)
16 | }
17 | })
18 |
19 | return result
20 | }
21 |
22 | const arrayToJson = (schema, result = {}, path = false) => {
23 | if (_.isPlainObject(schema) && _.startsWith(_.findKey(schema), '-')) {
24 | let array = []
25 | schema = _.sortBy(schema, '_order')
26 |
27 | _.each(schema, (value, key) => {
28 | value = _.omit(value, ['_order'])
29 | array.push(value)
30 | })
31 |
32 | _.set(result, path, array)
33 | }
34 |
35 | _.each(schema, (value, key) => {
36 | if (_.isPlainObject(value)) {
37 | let newPath = (!path) ? key : `${path}.${key}`
38 | arrayToJson(value, result, newPath)
39 | }
40 | })
41 |
42 | return result
43 | }
44 |
45 | export default (content) => {
46 | content = basicToJson(content)
47 | content = _.merge(content, arrayToJson(content))
48 |
49 | return content
50 | }
51 |
--------------------------------------------------------------------------------
/src/utils/codemirror.js:
--------------------------------------------------------------------------------
1 | // Modes
2 | // import 'codemirror/mode/javascript/javascript'
3 | // import 'codemirror/mode/xml/xml'
4 | // import 'codemirror/mode/css/css'
5 | // import 'codemirror/mode/sass/sass'
6 | // import 'codemirror/mode/markdown/markdown'
7 |
8 | // Style
9 | // import 'codemirror/lib/codemirror.css'
10 | // import 'codemirror/theme/dracula.css'
11 |
--------------------------------------------------------------------------------
/src/utils/firebase.js:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase/app'
2 | import 'firebase/auth'
3 | import 'firebase/firestore'
4 | import 'firebase/storage'
5 |
6 | firebase.initializeApp({
7 | apiKey: process.env.VUE_APP_FIREBASE_API_KEY,
8 | authDomain: process.env.VUE_APP_FIREBASE_AUTH_DOMAIN,
9 | databaseURL: process.env.VUE_APP_FIREBASE_DATABASE_URL,
10 | projectId: process.env.VUE_APP_FIREBASE_PROJECT_ID,
11 | storageBucket: process.env.VUE_APP_FIREBASE_STORAGE_BUCKET,
12 | })
13 |
14 | const firestore = firebase.firestore()
15 | firestore.settings({ timestampsInSnapshots: true })
16 |
17 | export default {
18 | firestore: firestore,
19 | firestoreDelete: firebase.firestore.FieldValue.delete(),
20 | firestoreTimestamp: firebase.firestore.FieldValue.serverTimestamp(),
21 | auth: firebase.auth(),
22 | storage: firebase.storage(),
23 | }
24 |
--------------------------------------------------------------------------------
/src/utils/icons.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Icon from 'vue-awesome/components/Icon'
3 |
4 | import 'vue-awesome/icons/regular/file'
5 | import 'vue-awesome/icons/regular/user'
6 | import 'vue-awesome/icons/check'
7 | import 'vue-awesome/icons/cloud-upload-alt'
8 | import 'vue-awesome/icons/spinner'
9 | import 'vue-awesome/icons/chevron-left'
10 | import 'vue-awesome/icons/chevron-right'
11 | import 'vue-awesome/icons/regular/edit'
12 | import 'vue-awesome/icons/cogs'
13 | import 'vue-awesome/icons/home'
14 | import 'vue-awesome/icons/trash'
15 | import 'vue-awesome/icons/regular/clock'
16 | import 'vue-awesome/icons/plus'
17 | import 'vue-awesome/icons/edit'
18 | import 'vue-awesome/icons/sign-out-alt'
19 | import 'vue-awesome/icons/regular/copy'
20 | import 'vue-awesome/icons/regular/thumbs-up'
21 | import 'vue-awesome/icons/times'
22 | import 'vue-awesome/icons/kiwi-bird'
23 | import 'vue-awesome/icons/play-circle'
24 |
25 | Icon.register({
26 | editlayer: {
27 | width: 28,
28 | height: 20,
29 | paths: [
30 | { d: 'm13.8461538 15.8859592 10.8869191-5.238444 2.9592348 1.2250892v1.1380841l-13.8461539 6.433133-13.8461538-6.433133v-1.1380841l2.86214193-1.2250892z' },
31 | { d: 'm13.8461538 0 13.8461539 6.72571046v1.13808415l-13.8461539 6.43313299-13.8461538-6.43313299v-1.13808415z' },
32 | ],
33 | },
34 | })
35 |
36 | Icon.register({
37 | schema: {
38 | width: 25,
39 | height: 23,
40 | d: 'M3.337 15.132c0-1.597-1.113-2.396-3.337-2.396v-2.46c1.129 0 1.967-.195 2.515-.585.548-.391.822-.986.822-1.784V3.902c0-1.34.453-2.324 1.36-2.955C5.606.316 6.967 0 8.782 0v2.344c-.954.043-1.646.242-2.076.599-.431.356-.646.886-.646 1.59v3.825c0 1.708-.958 2.73-2.873 3.065v.154c1.915.31 2.873 1.327 2.873 3.052v3.85c0 .705.213 1.237.64 1.598.426.36 1.12.55 2.082.566V23c-1.932-.017-3.322-.354-4.171-1.01-.849-.658-1.273-1.72-1.273-3.188v-3.67zm18.502 3.953c0 1.34-.416 2.322-1.248 2.95-.832.626-2.164.948-3.995.965v-2.357c.794-.008 1.413-.174 1.856-.495.443-.322.665-.878.665-1.668v-3.374c0-1.039.222-1.842.665-2.408.443-.567 1.179-.94 2.207-1.12v-.155c-1.915-.335-2.872-1.357-2.872-3.065V4.533c0-.704-.19-1.234-.57-1.59-.381-.357-1.032-.556-1.951-.6V0c1.864 0 3.205.328 4.02.985.815.657 1.223 1.732 1.223 3.226v3.696c0 .859.265 1.468.796 1.829.532.36 1.32.54 2.365.54v2.46c-1.029 0-1.813.183-2.352.548-.54.364-.81.98-.81 1.848v3.953z',
41 | },
42 | })
43 |
44 | Vue.component('icon', Icon)
45 |
--------------------------------------------------------------------------------
/src/utils/webhook.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import axios from 'axios'
3 | import { Base64 } from 'js-base64'
4 |
5 | const errorHandler = (message) => {
6 | console.error(message)
7 | console.groupEnd()
8 | return false
9 | }
10 |
11 | export default (payload) => {
12 | console.group('Webhook started')
13 |
14 | console.log('jsonUrl', payload.jsonUrl)
15 |
16 | let config = {}
17 |
18 | if (_.includes(payload.configString, '{{VERSION_ID}}')) {
19 | payload.configString = _.replace(payload.configString, '{{VERSION_ID}}', payload.versionId)
20 | }
21 |
22 | if (_.includes(payload.configString, '{{BASE64_CONTENT}}')) {
23 | let base64Content = Base64.encode(JSON.stringify(payload.json))
24 | payload.configString = _.replace(payload.configString, '{{BASE64_CONTENT}}', base64Content)
25 | }
26 |
27 | if (_.includes(payload.configString, '{{PUBLISHER_EMAIL}}')) {
28 | payload.configString = _.replace(payload.configString, '{{PUBLISHER_EMAIL}}', payload.email)
29 | }
30 |
31 | try {
32 | config = JSON.parse(payload.configString)
33 | }
34 | catch (e) {
35 | return errorHandler('Syntax error: Webhook config is not valid JSON', payload.configString)
36 | }
37 |
38 | if (!_.isString(config.url)) {
39 | return errorHandler('"url" is required')
40 | }
41 |
42 | if (config.method !== 'post' && config.method !== 'get') {
43 | return errorHandler('"method" is required and it should be "post" or "get"')
44 | }
45 |
46 | if (config.method === 'post' && _.has(config, 'params')) {
47 | return errorHandler('With a method "post" you should use "data" insted of "params"')
48 | }
49 |
50 | if (config.method === 'get' && _.has(config, 'data')) {
51 | return errorHandler('With a method "get" you should use "params" insted of "data"')
52 | }
53 |
54 | // if (config.method === 'get' && config.data && !config.params) {
55 | // config.params = config.data
56 | // delete config.data
57 | // }
58 |
59 | console.log('Config:')
60 | console.log(config)
61 |
62 | return axios(config)
63 | .then((response) => {
64 | console.log('Response:')
65 | console.log(response)
66 | console.groupEnd()
67 | return true
68 | })
69 | .catch((error) => {
70 | return errorHandler(error)
71 | })
72 | }
73 |
--------------------------------------------------------------------------------
/src/views/Auth.vue:
--------------------------------------------------------------------------------
1 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
161 |
162 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
258 |
--------------------------------------------------------------------------------
/src/views/Content.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
54 |
--------------------------------------------------------------------------------
/src/views/Dashboard.vue:
--------------------------------------------------------------------------------
1 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
51 |
52 |
53 |
54 |
55 |
You don't have any projects. Create the first project by clicking the "New Project" button.
56 |
You don't have a permission to any projects. Come back later or ask more from the administrator.
57 |
58 |
59 |
60 |
61 |
62 |
63 |
66 |
67 |
68 |
69 |
122 |
--------------------------------------------------------------------------------
/src/views/Schema.vue:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 |
29 |
30 | Schema
31 |
32 |
33 |
34 |
35 |
36 |
37 |
50 |
--------------------------------------------------------------------------------
/src/views/Settings.vue:
--------------------------------------------------------------------------------
1 |
33 |
34 |
35 |
36 |
37 | Project settings
38 |
39 |
40 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
82 |
--------------------------------------------------------------------------------