mango-cms 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/bin/mango +4 -0
- package/frontend-starter/README.md +8 -0
- package/frontend-starter/dist/_redirects +1 -0
- package/frontend-starter/dist/assets/index.00922bd5.js +99 -0
- package/frontend-starter/dist/assets/index.1781f175.css +1 -0
- package/frontend-starter/dist/favicon.png +0 -0
- package/frontend-starter/dist/index.html +53 -0
- package/frontend-starter/dist/index.js +66 -0
- package/frontend-starter/index.html +25 -0
- package/frontend-starter/index.js +197 -0
- package/frontend-starter/package-lock.json +5454 -0
- package/frontend-starter/package.json +40 -0
- package/frontend-starter/postcss.config.js +6 -0
- package/frontend-starter/public/_redirects +1 -0
- package/frontend-starter/public/favicon.png +0 -0
- package/frontend-starter/public/index.js +66 -0
- package/frontend-starter/src/App.vue +27 -0
- package/frontend-starter/src/components/layout/login.vue +212 -0
- package/frontend-starter/src/components/layout/modal.vue +113 -0
- package/frontend-starter/src/components/layout/spinner.vue +17 -0
- package/frontend-starter/src/components/pages/404.vue +28 -0
- package/frontend-starter/src/components/pages/home.vue +74 -0
- package/frontend-starter/src/components/partials/button.vue +31 -0
- package/frontend-starter/src/helpers/Mango.vue +455 -0
- package/frontend-starter/src/helpers/breakpoints.js +34 -0
- package/frontend-starter/src/helpers/darkMode.js +38 -0
- package/frontend-starter/src/helpers/email.js +32 -0
- package/frontend-starter/src/helpers/formatPhone.js +18 -0
- package/frontend-starter/src/helpers/localDB.js +315 -0
- package/frontend-starter/src/helpers/mango.js +338 -0
- package/frontend-starter/src/helpers/model.js +9 -0
- package/frontend-starter/src/helpers/multiSelect.vue +252 -0
- package/frontend-starter/src/helpers/pills.vue +75 -0
- package/frontend-starter/src/helpers/reconnecting-websocket.js +357 -0
- package/frontend-starter/src/helpers/uploadFile.vue +157 -0
- package/frontend-starter/src/helpers/uploadFiles.vue +100 -0
- package/frontend-starter/src/helpers/uploadImages.vue +89 -0
- package/frontend-starter/src/helpers/user.js +40 -0
- package/frontend-starter/src/index.css +281 -0
- package/frontend-starter/src/main.js +145 -0
- package/frontend-starter/tailwind.config.js +46 -0
- package/frontend-starter/vite.config.js +10 -0
- package/frontend-starter/yarn.lock +3380 -0
- package/mango-cms-0.0.13.tgz +0 -0
- package/package.json +24 -0
- package/src/cli.js +93 -0
- package/src/default-config/automation/index.js +37 -0
- package/src/default-config/collections/examples.js +60 -0
- package/src/default-config/config/.collections.json +1 -0
- package/src/default-config/config/globalFields.js +15 -0
- package/src/default-config/config/settings.json +23 -0
- package/src/default-config/config/statuses.js +0 -0
- package/src/default-config/config/users.js +35 -0
- package/src/default-config/endpoints/index.js +19 -0
- package/src/default-config/fields/vimeo.js +36 -0
- package/src/default-config/hooks/test.js +5 -0
- package/src/default-config/plugins/mango-stand/index.js +206 -0
- package/src/main.js +278 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cfl-front",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"dev": "vite",
|
|
6
|
+
"build": "vite build",
|
|
7
|
+
"preview": "vite preview"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@headlessui/vue": "^1.6.7",
|
|
11
|
+
"@mapbox/mapbox-gl-geocoder": "^5.0.0",
|
|
12
|
+
"@sweetalert2/theme-dark": "^5.0.10",
|
|
13
|
+
"@tinymce/tinymce-vue": "^4",
|
|
14
|
+
"@vitejs/plugin-vue": "^2.2.0",
|
|
15
|
+
"@vueup/vue-quill": "^1.0.0-beta.7",
|
|
16
|
+
"@vueuse/motion": "^2.0.0-beta.12",
|
|
17
|
+
"algoliasearch": "^4.12.0",
|
|
18
|
+
"axios": "^0.24.0",
|
|
19
|
+
"canvas-confetti": "^1.5.1",
|
|
20
|
+
"datebook": "^7.0.8",
|
|
21
|
+
"dayjs": "^1.10.7",
|
|
22
|
+
"express": "^4.18.1",
|
|
23
|
+
"google-maps": "^4.3.3",
|
|
24
|
+
"mapbox-gl": "^2.7.0",
|
|
25
|
+
"sweetalert2": "^11.4.0",
|
|
26
|
+
"vue": "^3.2.37",
|
|
27
|
+
"vue-router": "4",
|
|
28
|
+
"vue-slider-component": "^4.0.0-beta.4",
|
|
29
|
+
"vue-upload-component": "^3.1.2",
|
|
30
|
+
"vue3-clipboard": "^1.0.0",
|
|
31
|
+
"vue3-touch-events": "^4.1.0",
|
|
32
|
+
"vuedraggable": "^4.1.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"autoprefixer": "^10.4.0",
|
|
36
|
+
"postcss": "^8.4.5",
|
|
37
|
+
"tailwindcss": "^3.0.2",
|
|
38
|
+
"vite": "^4.2.1"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/* /index.html 200
|
|
Binary file
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const axios = require('axios')
|
|
4
|
+
const settings = require('../../config/config/settings.json')
|
|
5
|
+
const collections = require('../../config/config/.collections.json')
|
|
6
|
+
|
|
7
|
+
let port = 1337
|
|
8
|
+
let metaComment = '<!-- Inject Meta -->'
|
|
9
|
+
let appPlaceholder = '<!--SSR-->'
|
|
10
|
+
|
|
11
|
+
let serve = async function (req, res) {
|
|
12
|
+
|
|
13
|
+
// Cookies
|
|
14
|
+
let cookies
|
|
15
|
+
const { headers: { cookie } } = req;
|
|
16
|
+
if (cookie) {
|
|
17
|
+
const values = cookie.split(';').reduce((res, item) => {
|
|
18
|
+
const data = item.trim().split('=');
|
|
19
|
+
return { ...res, [data[0]]: data[1] };
|
|
20
|
+
}, {});
|
|
21
|
+
cookies = values;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// User Data Injection
|
|
25
|
+
let index = fs.readFileSync('./index.html', 'utf8')
|
|
26
|
+
let authorization = cookies?.Authorization
|
|
27
|
+
if (authorization) {
|
|
28
|
+
|
|
29
|
+
let hash = authorization.split(':')[0]
|
|
30
|
+
let userId = authorization.split(':')[1]
|
|
31
|
+
let user = (await axios.get(`http://localhost:${settings.port}/members?search={"password.hash": "${hash}", "id": "${userId}"}`, { headers: { 'Authorization': authorization } }))
|
|
32
|
+
user = user?.data?.response?.[0]
|
|
33
|
+
if (user) index = index.replace('window.user = null;', `window.user = ${JSON.stringify(user)};`)
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Define the collection and ID
|
|
38
|
+
let collection = req.path.split('/')[1]
|
|
39
|
+
let id = req.path.split('/')[2] ? req.path.split('/')[2] : null
|
|
40
|
+
|
|
41
|
+
console.log('collection', collection)
|
|
42
|
+
console.log('id', id)
|
|
43
|
+
|
|
44
|
+
let entry = (await axios.get(`http://localhost:${settings.port}/${collection}/${id}`))?.data?.response
|
|
45
|
+
|
|
46
|
+
console.log('entry', entry)
|
|
47
|
+
|
|
48
|
+
if (!entry) return res.send(index)
|
|
49
|
+
|
|
50
|
+
// Main Content Injection
|
|
51
|
+
try { index = index.replace('window.mainEntry = null;', `window.mainEntry = ${JSON.stringify(entry)};`) }
|
|
52
|
+
catch (e) { console.log(e) }
|
|
53
|
+
|
|
54
|
+
return res.send(index)
|
|
55
|
+
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const app = express()
|
|
59
|
+
|
|
60
|
+
app.get(["/index.html", "/"], serve);
|
|
61
|
+
app.use(express.static('./'))
|
|
62
|
+
app.get('/*', serve)
|
|
63
|
+
|
|
64
|
+
app.listen(port, () => {
|
|
65
|
+
console.log(`Example app listening at http://localhost:${port}`)
|
|
66
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="{'dark':darkMode.enabled}">
|
|
3
|
+
<div class="relative from-white to-slate-50 bg-gradient-to-b dark:from-black dark:to-gray-700 dark:selection:bg-gray-400 dark:text-gray-400 w-full min-h-screen">
|
|
4
|
+
|
|
5
|
+
<RouterView v-slot="{ Component, route }" @navHidden="navHidden = $event">
|
|
6
|
+
<Suspense :timeout="200" >
|
|
7
|
+
<Component :is="Component" :key="route.meta.usePathKey ? route.path : undefined" />
|
|
8
|
+
<template #fallback><spinner /></template>
|
|
9
|
+
</Suspense>
|
|
10
|
+
</RouterView>
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script>
|
|
17
|
+
import login from './components/layout/login.vue'
|
|
18
|
+
export default {
|
|
19
|
+
components: { login },
|
|
20
|
+
inject: ['store','darkMode'],
|
|
21
|
+
data() {
|
|
22
|
+
return {
|
|
23
|
+
navHidden: false
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
</script>
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="w-full p-8 flex items-center">
|
|
3
|
+
<slot
|
|
4
|
+
:logout="logout"
|
|
5
|
+
:login="login"
|
|
6
|
+
:createAccount="createAccount"
|
|
7
|
+
:sendRecoveryEmail="sendRecoveryEmail"
|
|
8
|
+
:resetPassword="resetPassword"
|
|
9
|
+
>
|
|
10
|
+
<form @submit.stop.prevent class="rounded-lg flex flex-col space-y-4 w-full">
|
|
11
|
+
|
|
12
|
+
<template v-if="creatingAccount">
|
|
13
|
+
<div class="text-2xl">Create an Account</div>
|
|
14
|
+
<div class="text-sm text-gray-500">The first account you create is automatically made an admin account.</div>
|
|
15
|
+
|
|
16
|
+
<div class="flex gap-4 w-full">
|
|
17
|
+
<input type="text" v-model.trim="user.firstName" placeholder="First Name" />
|
|
18
|
+
<input type="text" v-model.trim="user.lastName" placeholder="Last Name" />
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<input type="email" v-model.trim="user.email" placeholder="Email" />
|
|
22
|
+
<input type="password" v-model.trim="user.password" placeholder="Password" autocomplete="new-password" />
|
|
23
|
+
|
|
24
|
+
<!-- <div><input type="text" v-model="user.address.address" placeholder="Address" /></div>
|
|
25
|
+
<div class="space-x-4 flex">
|
|
26
|
+
<input type="text" v-model="user.address.zip" placeholder="Zip Code" @input="extractZip" />
|
|
27
|
+
</div>
|
|
28
|
+
<div class="space-x-4 flex">
|
|
29
|
+
<input type="text" v-model="user.address.city" placeholder="City" />
|
|
30
|
+
<input type="text" v-model="user.address.state" placeholder="State" />
|
|
31
|
+
</div> -->
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<template v-else>
|
|
35
|
+
<div class="text-2xl">Mango Login</div>
|
|
36
|
+
<input type="email" v-model.trim="user.email" placeholder="Email" />
|
|
37
|
+
<input type="password" v-model.trim="user.password" placeholder="Password" autocomplete="current-password" />
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<button @click="action" class="px-3 py-2 bg-orange-500 dark:bg-orange-500/80 dark:text-white/75 text-white rounded flex justify-center">
|
|
41
|
+
<template v-if="!processing">{{ buttonText }}</template>
|
|
42
|
+
<Spinner v-else class="border-t-family-at-church-orange w-4 h-4 my-1" :small="true" color="border-t-white/50" />
|
|
43
|
+
</button>
|
|
44
|
+
|
|
45
|
+
<button v-if="store.user?.member?.id" @click="logout" class="text-xs text-gray-400 hover:underline">Logout</button>
|
|
46
|
+
|
|
47
|
+
<div class="text-center select-none" >
|
|
48
|
+
<button v-if="!creatingAccount" @click="creatingAccount = true; guest = false; forgotPassword = false" class="text-xs text-gray-400 hover:underline">Create Account</button>
|
|
49
|
+
<button v-if="creatingAccount || resettingPassword" @click="creatingAccount = false; guest = false; forgotPassword = false" class="text-xs text-gray-400 hover:underline" :class="{'ml-2 pl-2 border-l dark:border-gray-500': resettingPassword}">{{ resettingPassword || (allowGuest && !guest) ? 'Login' : 'Have an account? Login instead.' }}</button>
|
|
50
|
+
<button v-if="allowGuest && !guest" @click="creatingAccount = true; guest = true; forgotPassword = false" class="text-xs text-gray-400 hover:underline ml-2 pl-2 border-l dark:border-gray-500">Continue as Guest</button>
|
|
51
|
+
<button v-if="!creatingAccount && !resettingPassword" @click="creatingAccount = false; guest = false; forgotPassword = true" class="text-xs text-gray-400 hover:underline ml-2 pl-2 border-l dark:border-gray-500">Forgot Password?</button>
|
|
52
|
+
</div>
|
|
53
|
+
</form>
|
|
54
|
+
</slot>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<script>
|
|
59
|
+
import Swal from 'sweetalert2'
|
|
60
|
+
import { validateEmail } from '../../helpers/email'
|
|
61
|
+
import { getUser } from '../../helpers/user'
|
|
62
|
+
import Mango from '../../helpers/mango'
|
|
63
|
+
|
|
64
|
+
// Function for setting cookies
|
|
65
|
+
let setCookie = function (cname, cvalue) {
|
|
66
|
+
var d = new Date();
|
|
67
|
+
d.setTime(d.getTime() + (365 * 24 * 60 * 60 * 1000));
|
|
68
|
+
var expires = "expires=" + d.toUTCString();
|
|
69
|
+
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default {
|
|
73
|
+
inject: ['store','axios'],
|
|
74
|
+
data() {
|
|
75
|
+
return {
|
|
76
|
+
user: {
|
|
77
|
+
email: null,
|
|
78
|
+
password: null,
|
|
79
|
+
firstName: null,
|
|
80
|
+
lastName: null,
|
|
81
|
+
// address: {
|
|
82
|
+
// name: null,
|
|
83
|
+
// address: null,
|
|
84
|
+
// city: null,
|
|
85
|
+
// state: null,
|
|
86
|
+
// zip: null,
|
|
87
|
+
// },
|
|
88
|
+
},
|
|
89
|
+
processing: false,
|
|
90
|
+
creatingAccount: true,
|
|
91
|
+
guest: false,
|
|
92
|
+
forgotPassword: false,
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
watch: {
|
|
96
|
+
loggedIn() {
|
|
97
|
+
this.$emit('hide')
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
methods: {
|
|
101
|
+
validateEmail,
|
|
102
|
+
async login() {
|
|
103
|
+
this.processing = true
|
|
104
|
+
let member = (await Mango.login({ email: this.user.email, password: this.user.password }))
|
|
105
|
+
|
|
106
|
+
if (member?.memberId) {
|
|
107
|
+
if (member.roles?.includes('admin')) member.admin = true
|
|
108
|
+
|
|
109
|
+
window.localStorage.setItem('user', member.memberId)
|
|
110
|
+
window.localStorage.setItem('token', member.token)
|
|
111
|
+
window.localStorage.setItem('email', this.user.email)
|
|
112
|
+
|
|
113
|
+
setCookie(`Authorization`, `${member.token}`)
|
|
114
|
+
this.axios.defaults.headers.common['Authorization'] = `${member.token}`
|
|
115
|
+
|
|
116
|
+
const user = await getUser()
|
|
117
|
+
this.store.user = user
|
|
118
|
+
if (user?.company?.id) this.store.theme = user.company
|
|
119
|
+
|
|
120
|
+
this.$emit('loggedIn')
|
|
121
|
+
|
|
122
|
+
let restrictedPath = this.store?.login?.next || '/'
|
|
123
|
+
if (restrictedPath) this.$router.push(restrictedPath)
|
|
124
|
+
|
|
125
|
+
this.$emit('hide')
|
|
126
|
+
} else if (member?.invalidFields) {
|
|
127
|
+
Swal.fire({ title: `Invalid ${member.invalidFields.join(', ')}`, icon: 'error' })
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.processing = false
|
|
131
|
+
},
|
|
132
|
+
logout() {
|
|
133
|
+
this.store.user = {}
|
|
134
|
+
window.user = null
|
|
135
|
+
window.localStorage.removeItem('user')
|
|
136
|
+
setCookie(`Authorization`, ``)
|
|
137
|
+
delete this.axios.defaults.headers.common['Authorization']
|
|
138
|
+
this.$emit('loggedOut')
|
|
139
|
+
this.$emit('hide')
|
|
140
|
+
this.$router.push('/login')
|
|
141
|
+
},
|
|
142
|
+
async createAccount() {
|
|
143
|
+
if (!this.validateEmail(this.user.email)) return Swal.fire('Email must be a valid email.')
|
|
144
|
+
if (this.user.password.length < 6 && !this.guest) return Swal.fire('Password must be at least 6 characters.')
|
|
145
|
+
this.processing = true
|
|
146
|
+
|
|
147
|
+
var data = {
|
|
148
|
+
...this.user,
|
|
149
|
+
title: `${this.user.firstName} ${this.user.lastName || ''}`.trim()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let response = await Mango.members.save(data)
|
|
153
|
+
if (!response.id) {
|
|
154
|
+
Swal.fire('Account Exists', response, 'info')
|
|
155
|
+
} else {
|
|
156
|
+
await this.login()
|
|
157
|
+
}
|
|
158
|
+
this.$emit('accountCreated')
|
|
159
|
+
this.$emit('hide')
|
|
160
|
+
this.processing = false
|
|
161
|
+
},
|
|
162
|
+
async sendRecoveryEmail () {
|
|
163
|
+
this.processing = true
|
|
164
|
+
let response = await this.axios.post(`${this.store.api}/controllers/account/sendResetInstructions`, {email: this.user.email, forgot: true})
|
|
165
|
+
if (response.data.success) Swal.fire('Success!', `Recovery instructions sent to: ${this.user.email}`, 'success')
|
|
166
|
+
else Swal.fire('Invalid Email!', `The following user does not exist: ${this.user.email}`, 'warning')
|
|
167
|
+
this.$emit('emailSent')
|
|
168
|
+
this.$emit('hide')
|
|
169
|
+
this.processing = false
|
|
170
|
+
},
|
|
171
|
+
async resetPassword () {
|
|
172
|
+
if (this.user.password.length < 6) return Swal.fire('Password must be at least 6 characters.')
|
|
173
|
+
this.processing = true
|
|
174
|
+
|
|
175
|
+
let data = {
|
|
176
|
+
email: this.$route.query.email,
|
|
177
|
+
salt: this.$route.query.salt,
|
|
178
|
+
password: this.user.password,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let response = await this.axios.post(`${this.store.api}/controllers/account/resetPassword`, data)
|
|
182
|
+
if (response.data.success) {
|
|
183
|
+
this.user.email = this.$route.query.email
|
|
184
|
+
await this.login()
|
|
185
|
+
this.$router.push({query: null})
|
|
186
|
+
swal('Success!', 'Your password has been reset.', 'success')
|
|
187
|
+
} else {
|
|
188
|
+
swal('Invalid Link!', `The link you're using is invalid.`, 'error')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.processing = false
|
|
192
|
+
this.$emit('passwordReset')
|
|
193
|
+
this.$emit('hide')
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
computed: {
|
|
197
|
+
guestPassword() { return Math.random().toString(36).substr(2)+Math.random().toString(36).substr(2) },
|
|
198
|
+
labelText() { return this.resettingPassword ? 'Reset Password' : this.forgotPassword ? 'Recover Password' : this.creatingAccount ? this.guest ? 'Continue as Guest' : 'Create Account' : 'Login' },
|
|
199
|
+
buttonText() { return this.resettingPassword ? 'Update Password' : this.forgotPassword ? 'Send Recovery Email' : this.creatingAccount ? this.guest ? 'Continue' : 'Create' : 'Login' },
|
|
200
|
+
action() { return this.resettingPassword ? this.resetPassword : this.forgotPassword ? this.sendRecoveryEmail : this.creatingAccount ? this.createAccount : this.login },
|
|
201
|
+
resettingPassword() { return !!this.$route.query?.salt },
|
|
202
|
+
loggedIn() { return !!this.store.user?.id }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
</script>
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
<style lang="postcss" scoped>
|
|
209
|
+
input {
|
|
210
|
+
@apply border rounded outline-blue-400 px-3 py-2 w-full dark:bg-transparent dark:border-gray-600 dark:placeholder-gray-600
|
|
211
|
+
}
|
|
212
|
+
</style>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
ref="modal"
|
|
4
|
+
@click.prevent.stop="close"
|
|
5
|
+
tabindex="-1"
|
|
6
|
+
class="
|
|
7
|
+
fixed w-full h-screen bg-gray-800/50 dark:bg-black/60 backdrop-blur-sm md:backdrop-blur-md opacity-0 transition-all duration-500
|
|
8
|
+
flex items-start sm:items-center justify-center z-[100] inset-0 !m-0 overscroll-none cursor-default
|
|
9
|
+
"
|
|
10
|
+
:class="{'!opacity-100': fadeIn}"
|
|
11
|
+
aria-modal="true"
|
|
12
|
+
v-show="active"
|
|
13
|
+
role="dialog"
|
|
14
|
+
>
|
|
15
|
+
<button @click.prevent.stop="close" class="absolute top-2 right-3"><i class="fa fa-times md:text-4xl" /></button>
|
|
16
|
+
<div
|
|
17
|
+
ref="modalContent"
|
|
18
|
+
@click.stop
|
|
19
|
+
class="shadow-card rounded w-full p-4 md:p-8 m-2 dark:bg-gray-800 bg-white relative
|
|
20
|
+
border dark:border-gray-700 space-y-4 md:space-y-8 max-h-[75vh]"
|
|
21
|
+
:class="[dialogClasses, maxWidth, allowOverflow ? 'overflow-visible' : 'overflow-y-scroll']"
|
|
22
|
+
>
|
|
23
|
+
<slot :close="close"/>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<!-- max-w-md max-w-lg max-w-xl max-w-sm -->
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<script>
|
|
30
|
+
export default {
|
|
31
|
+
props: {
|
|
32
|
+
dialogClasses: {type: String, default: ''},
|
|
33
|
+
maxWidth: {default: 'max-w-md'},
|
|
34
|
+
active: {default: true},
|
|
35
|
+
allowOverflow: {type: Boolean, default: false},
|
|
36
|
+
},
|
|
37
|
+
data() {
|
|
38
|
+
return {
|
|
39
|
+
fadeIn: false
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
watch: {
|
|
43
|
+
active: {
|
|
44
|
+
handler() {
|
|
45
|
+
if (this.active) this.freeze()
|
|
46
|
+
else this.thaw()
|
|
47
|
+
},
|
|
48
|
+
immediate: true
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
beforeDestroy() {
|
|
52
|
+
this.thaw();
|
|
53
|
+
},
|
|
54
|
+
unmounted() {
|
|
55
|
+
this.thaw();
|
|
56
|
+
},
|
|
57
|
+
methods: {
|
|
58
|
+
freeze() {
|
|
59
|
+
this.$nextTick(() => {
|
|
60
|
+
this.$refs.modalContent.focus()
|
|
61
|
+
this.fadeIn = true
|
|
62
|
+
document.body.style.overflow = 'hidden';
|
|
63
|
+
document.getElementById('app').setAttribute('aria-hidden', 'true');
|
|
64
|
+
this.$refs.modal.setAttribute('aria-modal', 'true');
|
|
65
|
+
this.attachListeners();
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
thaw() {
|
|
69
|
+
document.body.style.overflow = '';
|
|
70
|
+
document.getElementById('app').setAttribute('aria-hidden', 'false');
|
|
71
|
+
this.$refs?.modal?.removeAttribute('aria-modal');
|
|
72
|
+
this.removeListeners();
|
|
73
|
+
},
|
|
74
|
+
close() {
|
|
75
|
+
this.fadeIn = false
|
|
76
|
+
this.thaw()
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
this.$emit('hide')
|
|
79
|
+
}, 500);
|
|
80
|
+
},
|
|
81
|
+
attachListeners() {
|
|
82
|
+
window.addEventListener('keydown', this.handleKeydown);
|
|
83
|
+
},
|
|
84
|
+
removeListeners() {
|
|
85
|
+
window.removeEventListener('keydown', this.handleKeydown);
|
|
86
|
+
},
|
|
87
|
+
handleKeydown(e) {
|
|
88
|
+
if (e.key === 'Tab') {
|
|
89
|
+
this.manageFocus(e);
|
|
90
|
+
} else if (e.key === 'Escape') {
|
|
91
|
+
this.close()
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
manageFocus(e) {
|
|
95
|
+
let focusable = Array.from(this.$refs.modal.querySelectorAll('button, [href], input:not([type="hidden"]), select, textarea, [tabindex]:not([tabindex="-1"])'));
|
|
96
|
+
focusable = focusable.filter(el => window.getComputedStyle(el).display !== 'none');
|
|
97
|
+
console.log('focusable', focusable)
|
|
98
|
+
|
|
99
|
+
if (!focusable.includes(document.activeElement)) {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
focusable[0].focus();
|
|
102
|
+
} else if (e.shiftKey && document.activeElement === focusable[0]) {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
focusable[focusable.length - 1].focus();
|
|
105
|
+
} else if (document.activeElement === focusable[focusable.length - 1]) {
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
focusable[0].focus();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
</script>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="flex justify-center items-center border-transparent"
|
|
4
|
+
:class="{'w-full h-screen': !small}"
|
|
5
|
+
>
|
|
6
|
+
<div class="rounded-full border-4 animate-spin w-full h-full max-w-32 max-h-32 border-inherit" :class="color"/>
|
|
7
|
+
</div>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script>
|
|
11
|
+
export default {
|
|
12
|
+
props: {
|
|
13
|
+
small: {type: Boolean, default: false},
|
|
14
|
+
color: {default: 'border-t-blue-500'}
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
</script>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<main class="w-full pt-16 flex flex-col justify-center items-center text-center min-h-screen">
|
|
3
|
+
<div class="w-full flex flex-wrap justify-center items-center relative px-8 md:px-16">
|
|
4
|
+
<h1 class="w-full text-8xl sm:text-9xl md:text-10xl font-bold text-gray-800 leading-none">404</h1>
|
|
5
|
+
<p class="w-full text-4xl font-semibold text-gray-600">Are you lost?</p>
|
|
6
|
+
<p class="w-full text-2xl font-semibold text-gray-600 mt-4">
|
|
7
|
+
How about messages on
|
|
8
|
+
<router-link
|
|
9
|
+
class="text-3xl text-ncfic-blue-500 dark:text-ncfic-blue-800 hover:text-ncfic-blue-600 font-bold hover:underline hover:-translate-y-2 transition-all duration-500 inline-block"
|
|
10
|
+
:to="`/resources/topics/60c927e8826ba92e9196fcd5`"
|
|
11
|
+
>
|
|
12
|
+
The Gospel
|
|
13
|
+
<i class="fas fa-long-arrow-right text-gray-200 dark:text-gray-800"></i>
|
|
14
|
+
</router-link>
|
|
15
|
+
</p>
|
|
16
|
+
</div>
|
|
17
|
+
</main>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script>
|
|
21
|
+
export default {
|
|
22
|
+
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<style>
|
|
27
|
+
|
|
28
|
+
</style>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
|
|
3
|
+
<div class="relative">
|
|
4
|
+
|
|
5
|
+
<div class="fixed top-4 left-4 rounded border border-gray-400 dark:border-gray-600 dark:bg-black px-2 flex space-x-2 items-center">
|
|
6
|
+
<div class="rounded-full w-2 h-2 shrink-0" :class="online ? 'bg-green-500' : 'bg-red-500'" />
|
|
7
|
+
<div>{{ online ? 'Online' : 'Offline' }}</div>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div class="pt-16 lg:pt-32 flex items-center justify-center w-full">
|
|
11
|
+
|
|
12
|
+
<div class="w-full max-2xl py-16 md:py-32 text-center flex flex-col items-center px-8 md:px-16">
|
|
13
|
+
<div class="text-9xl pb-8">ðŸ¥</div>
|
|
14
|
+
<h1 class="font-bold text-5xl pb-4 bg-clip-text bg-gradient-to-r from-orange-500 to-green-500 text-transparent">Welcome to Mango</h1>
|
|
15
|
+
<h1 class="font-bold text-lg bg-clip-text bg-gradient-to-r from-orange-500 to-green-500 text-transparent italic opacity-50">Here to make your life stinking easy.</h1>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div v-if="!store.user?.id || !online" class="flex w-full justify-center">
|
|
21
|
+
<login v-if="online" class="max-w-md" />
|
|
22
|
+
<div v-else class="space-y-8">
|
|
23
|
+
<div class="text-lg">Looks like the local Mango server isn't up and running yet. Please start it by running:</div>
|
|
24
|
+
<div class="w-full">
|
|
25
|
+
<code class="bg-gray-200 dark:bg-black p-3 border border-gray-700 rounded-lg w-full">cd mango; yarn; yarn watch;</code>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div v-else-if="store.user?.id" class="w-full flex flex-col p-4 items-center space-y-4">
|
|
31
|
+
|
|
32
|
+
<div class="w-full max-w-xl">
|
|
33
|
+
<div class="text mb-4">Great! Now you're logged in as:</div>
|
|
34
|
+
<div class="text-2xl mb-8">{{ store.user.title }}</div>
|
|
35
|
+
<div class="rounded-lg bg-gray-100 dark:bg-gray-800 p-4 ">
|
|
36
|
+
<div class="tracking-widest opacity-75 font-mono mt-2 truncate" v-html="JSON.stringify(store.user, undefined, 4).replaceAll('\n', '<br>').replaceAll(' ', ' ')" />
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<!-- <Mango collection="members" v-slot="{data, loading}" >
|
|
41
|
+
<div v-if="data">
|
|
42
|
+
<div v-for="member in data">{{ member.title }}</div>
|
|
43
|
+
</div>
|
|
44
|
+
</Mango> -->
|
|
45
|
+
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<script>
|
|
52
|
+
import Mango from '../../helpers/mango'
|
|
53
|
+
import Login from '../layout/login.vue'
|
|
54
|
+
|
|
55
|
+
export default {
|
|
56
|
+
components: { Login },
|
|
57
|
+
inject: ['store'],
|
|
58
|
+
data() {
|
|
59
|
+
return {
|
|
60
|
+
online: false,
|
|
61
|
+
checker: null,
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
async created() {
|
|
65
|
+
this.online = await Mango.online()
|
|
66
|
+
this.checker = setInterval(async () => {
|
|
67
|
+
this.online = await Mango.online()
|
|
68
|
+
}, 500);
|
|
69
|
+
},
|
|
70
|
+
beforeDestroy() {
|
|
71
|
+
clearInterval(this.checker)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
</script>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative pb-[4px]">
|
|
3
|
+
<button
|
|
4
|
+
class="relative z-10 w-full text-3xl rounded-xl text-white uppercase"
|
|
5
|
+
:class="[`bg-${store.theme.color}-500`, selected ? 'py-4 px-6' : 'py-6 px-8', selected ? `border-8 border-${store.theme.color}-400`: '']"
|
|
6
|
+
>
|
|
7
|
+
{{ label }}
|
|
8
|
+
</button>
|
|
9
|
+
<div :class="`bg-${store.theme.color}-600`" class="absolute bottom-0 w-full text-3xl px-8 py-6 rounded-xl"> </div>
|
|
10
|
+
<div :class="`bg-${store.theme.color}-400`" class="absolute z-20 -top-2 -right-2 rounded-full flex items-center justify-center w-8 h-8 shrink-0 text-white" v-if="selected">
|
|
11
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="4" stroke="currentColor" class="w-4 h-4">
|
|
12
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
|
13
|
+
</svg>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script>
|
|
19
|
+
export default {
|
|
20
|
+
inject: ['store'],
|
|
21
|
+
props: {
|
|
22
|
+
label: {
|
|
23
|
+
type: String,
|
|
24
|
+
},
|
|
25
|
+
selected: {
|
|
26
|
+
type: Boolean,
|
|
27
|
+
default: false,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
</script>
|