Last active
January 6, 2024 23:09
-
-
Save jeff-silva/f48fcbfc7a166f1d77018b78b7271d82 to your computer and use it in GitHub Desktop.
Vue3 Helpers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Install dependencies | |
* yarn add -D axios | |
*/ | |
import axios from "axios"; | |
import { computed } from "vue"; | |
export default (options = {}) => { | |
options = { | |
autoSubmit: false, | |
url: false, | |
method: false, | |
params: {}, | |
data: {}, | |
response: false, | |
events: [], | |
filters: {}, | |
...options, | |
}; | |
["url", "params", "method", "data"].map((attr) => { | |
if (typeof options[attr] == "function") { | |
options[attr] = computed(options[attr]); | |
} | |
}); | |
axios.defaults.headers["Content-Type"] = null; | |
const r = reactive({ | |
...options, | |
ready: false, | |
busy: false, | |
error: false, | |
success: false, | |
t: false, | |
errorField(field) { | |
if (!r.error) return []; | |
if (!r.error.fields) return []; | |
return r.error.fields[field] || []; | |
}, | |
submit() { | |
if (r.busy) return; | |
return new Promise((resolve, reject) => { | |
if (r.t) { | |
clearTimeout(r.t); | |
r.t = false; | |
} | |
r.success = false; | |
r.error = false; | |
r.busy = true; | |
r.dispatch("beforeSubmit"); | |
r.t = setTimeout(async () => { | |
try { | |
r.dispatch("submit"); | |
const resp = await axios({ | |
url: r.url, | |
method: r.method, | |
params: r.params, | |
data: r.data, | |
}); | |
r.dispatch("success", resp); | |
r.dispatch("response", resp, false); | |
r.response = r.filters.apply("response", resp).data; | |
r.success = true; | |
resolve(resp); | |
} catch (err) { | |
r.error = err.response | |
? err.response.data | |
: { message: err.message }; | |
r.dispatch("error", r.error); | |
r.dispatch("response", false, r.error); | |
reject(err); | |
} | |
r.t = false; | |
r.busy = false; | |
r.ready = true; | |
}, 1000); | |
}); | |
}, | |
events: options.events, | |
on(name, call) { | |
this.events.push({ name, call }); | |
}, | |
dispatch(eventName, args = []) { | |
this.events.map(({ name, call }) => { | |
if (name != eventName) return; | |
return call.apply(null, args); | |
}); | |
}, | |
/** | |
* const instance = useAxios({ | |
* filters: { | |
* response(resp) { | |
* resp.data.data = resp.data.data.reverse(); | |
* return resp; | |
* }, | |
* }, | |
* }); | |
* | |
* or | |
* | |
* instance.add('response', (resp) => { | |
* resp.data.data = resp.data.data.reverse(); | |
* return resp; | |
* }); | |
*/ | |
filters: { | |
...options.filters, | |
add(name, call) { | |
r.filter.list[name] = call; | |
}, | |
apply(name, value) { | |
const ignore = ["add", "apply"]; | |
if (typeof r.filters[name] == "function" && !ignore.includes(name)) { | |
return r.filters[name](value); | |
} | |
return value; | |
}, | |
}, | |
}); | |
r.dispatch("init"); | |
if (r.autoSubmit) { | |
r.submit(); | |
} | |
return r; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 1) Install dependencies | |
* yarn add -D firebase | |
* | |
* 2) In Firebase panel, create project then set variables in .env | |
* FIREBASE_API_KEY= | |
* FIREBASE_AUTH_DOMAIN= | |
* FIREBASE_PROJECT_ID= | |
* FIREBASE_STORAGE_BUCKET= | |
* FIREBASE_MESSAGING_SENDER_ID= | |
* FIREBASE_APP_ID= | |
* FIREBASE_MEASUREMENT_ID= | |
* | |
* 3) Insert into nuxt.config.ts | |
* Dont forget to put the variables above in docker-compose.yml service "environment" attribute. | |
* | |
* runtimeConfig: { | |
public: { | |
firebase: { | |
apiKey: process.env.FIREBASE_API_KEY, | |
authDomain: process.env.FIREBASE_AUTH_DOMAIN, | |
projectId: process.env.FIREBASE_PROJECT_ID, | |
appId: process.env.FIREBASE_APP_ID, | |
storageBucket: process.env.FIREBASE_STORAGE_BUCKET, | |
messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID, | |
measurementId: process.env.FIREBASE_MEASUREMENT_ID, | |
}, | |
}, | |
}, | |
* | |
* 4) Create the file with this content | |
* composables/useFirebase.js | |
* | |
* 5) Set the Firebase configurations, example: | |
* - Activate and configure Authentication | |
* - Activate and configure Firestore database with permissions | |
* allow read: if true; | |
* allow write: if request.auth != null; | |
*/ | |
import { ref, reactive } from "vue"; | |
import { defineStore } from "pinia"; | |
import * as fireApp from "firebase/app"; | |
import * as fireAuth from "firebase/auth"; | |
import * as fireFirestore from "firebase/firestore"; | |
import * as fireStorage from "firebase/storage"; | |
export default defineStore("firebase", () => { | |
const config = useRuntimeConfig(); | |
fireApp.initializeApp(config.public.firebase); | |
const _auth = fireAuth.getAuth(); | |
const _storage = fireStorage.getStorage(); | |
const fireFirestoreDB = fireFirestore.getFirestore(); | |
const strategies = { | |
login: { | |
async email(data) { | |
return await fireAuth.signInWithEmailAndPassword( | |
_auth, | |
data.email, | |
data.password | |
); | |
}, | |
}, | |
register: { | |
async email(data) { | |
return await fireAuth.createUserWithEmailAndPassword( | |
_auth, | |
data.email, | |
data.password | |
); | |
}, | |
}, | |
}; | |
const ready = ref(false); | |
const user = ref(false); | |
const auth = reactive({ | |
busy: false, | |
success: false, | |
error: false, | |
async register(data = {}, strategy = "email") { | |
data = { email: "", password: "", ...data }; | |
this.busy = true; | |
this.success = false; | |
this.error = false; | |
try { | |
if (typeof strategies["register"][strategy] != "undefined") { | |
await strategies["register"][strategy](data); | |
this.success = true; | |
event.dispatch("registerSuccess"); | |
} | |
} catch (err) { | |
this.error = await this.exception(err); | |
event.dispatch("registerError"); | |
} | |
this.busy = false; | |
}, | |
async login(data = {}, strategy = "email") { | |
data = { email: "", password: "", ...data }; | |
this.busy = true; | |
this.success = false; | |
this.error = false; | |
try { | |
if (typeof strategies["login"][strategy] != "undefined") { | |
await strategies["login"][strategy](data); | |
this.success = true; | |
event.dispatch("loginSuccess"); | |
} | |
} catch (err) { | |
this.error = await this.exception(err); | |
event.dispatch("loginError"); | |
} | |
this.busy = false; | |
}, | |
async logout() { | |
const r = await fireAuth.signOut(_auth); | |
event.dispatch("logout"); | |
return r; | |
}, | |
async update(data = {}) { | |
data = { name: "", email: "", phoneNumber: "", photoURL: "", ...data }; | |
const profileUpdate = !( | |
data.name == _auth.currentUser.displayName && | |
data.photoURL == _auth.currentUser.photoURL | |
); | |
const emailUpdate = !(data.email == _auth.currentUser.email); | |
const phoneNumberUpdate = !( | |
data.phoneNumber == _auth.currentUser.phoneNumber | |
); | |
if (profileUpdate) { | |
await fireAuth.updateProfile(_auth.currentUser, { | |
displayName: data.name, | |
photoURL: data.photoURL, | |
}); | |
} | |
if (emailUpdate) { | |
await fireAuth.updateEmail(_auth.currentUser, data.email); | |
} | |
}, | |
async exception(err) { | |
return { code: "", message: "", customData: {}, name: false, ...err }; | |
}, | |
}); | |
const firestore = reactive({ | |
busy: false, | |
error: false, | |
async find(collection, value, by = "uid") { | |
const docRef = fireFirestore.doc(fireFirestoreDB, collection, value); | |
const docSnap = await fireFirestore.getDoc(docRef); | |
return docSnap.exists() ? docSnap.data() : false; | |
}, | |
async save(collection, data = {}) { | |
data = { uid: null, name: "", ...data }; | |
const ref = fireFirestore.collection(fireFirestoreDB, collection); | |
if (!data.uid) { | |
const created = await fireFirestore.addDoc(ref, data); | |
data.uid = created.id; | |
} | |
await fireFirestore.setDoc( | |
fireFirestore.doc(fireFirestoreDB, collection, data.uid), | |
data | |
); | |
return data; | |
}, | |
async delete(collection, value, by = "uid") {}, | |
async search(collection, query = {}) { | |
query = { | |
limit: 5, | |
// orderBy: ["uid", "desc"], | |
where: [], | |
startAfter: false, | |
endAt: false, | |
...query, | |
}; | |
let prev = false; | |
let next = false; | |
const collectRef = fireFirestore.collection(fireFirestoreDB, collection); | |
let queryArgs = []; | |
if (query.orderBy) { | |
queryArgs.push(fireFirestore.orderBy.apply(null, query.orderBy)); | |
} | |
if (query.where.length > 0) { | |
query.where.map((condition) => { | |
queryArgs.push(fireFirestore.where.apply(null, condition)); | |
}); | |
} | |
if (query.startAfter) { | |
queryArgs.push( | |
fireFirestore.startAfter( | |
await fireFirestore.getDoc( | |
fireFirestore.doc(fireFirestoreDB, collection, query.startAfter) | |
) | |
) | |
); | |
} | |
if (query.endAt) { | |
queryArgs.push( | |
fireFirestore.endAt( | |
await fireFirestore.getDoc( | |
fireFirestore.doc(fireFirestoreDB, collection, query.endAt) | |
) | |
) | |
); | |
} | |
if (query.limit) { | |
queryArgs.push(fireFirestore.limit(query.limit)); | |
} | |
this.busy = true; | |
const docsQuery = fireFirestore.query.apply(null, [ | |
collectRef, | |
...queryArgs, | |
]); | |
const docs = await fireFirestore.getDocs(docsQuery); | |
let data = []; | |
docs.forEach((doc) => { | |
data.push({ ...doc.data(), uid: doc.id }); | |
}); | |
if (data.length == query.limit) { | |
next = JSON.parse(JSON.stringify(query)); | |
next.startAfter = data[data.length - 1]["uid"]; | |
next.endAt = false; | |
} | |
this.busy = false; | |
return { query, data, prev, next }; | |
}, | |
async onSnapshot(collection, uid, callback) { | |
const doc = fireFirestore.doc(fireFirestoreDB, collection, uid); | |
return fireFirestore.onSnapshot(doc, callback); | |
}, | |
}); | |
const storage = reactive({ | |
busy: false, | |
error: false, | |
async upload(file) { | |
if (file instanceof File) { | |
const storageRef = fireStorage.ref(_storage, file.name); | |
const snapshot = await fireStorage.uploadBytes(storageRef, file); | |
const url = await fireStorage.getDownloadURL(snapshot.ref); | |
return { url, snapshot }; | |
} | |
return false; | |
}, | |
}); | |
const event = reactive({ | |
events: [], | |
async on(name, callback) { | |
this.events.push({ name, callback }); | |
}, | |
async dispatch(eventName) { | |
this.events.map(({ name, callback }) => { | |
if (name != eventName) return; | |
callback(); | |
}); | |
}, | |
}); | |
fireAuth.onAuthStateChanged(_auth, (authUser) => { | |
ready.value = true; | |
if (!authUser) { | |
user.value = false; | |
event.dispatch("onAuthStateChanged"); | |
return; | |
} | |
user.value = { | |
uid: authUser.uid, | |
name: | |
(authUser.providerData[0] | |
? authUser.providerData[0]["displayName"] | |
: null) || authUser.email, | |
email: authUser.email, | |
emailVerified: authUser.emailVerified, | |
phoneNumber: authUser.providerData[0] | |
? authUser.providerData[0]["phoneNumber"] | |
: "", | |
photoURL: authUser.providerData[0] | |
? authUser.providerData[0]["photoURL"] | |
: "", | |
}; | |
event.dispatch("onAuthStateChanged"); | |
}); | |
return { ready, user, auth, firestore, storage, event }; | |
// const r = ref({ | |
// ready: false, | |
// user: false, | |
// app: {}, | |
// auth: { | |
// busy: false, | |
// error: false, | |
// async register(data = {}, strategy = "email") { | |
// data = { email: "", password: "", ...data }; | |
// this.busy = true; | |
// try { | |
// if (typeof strategies["register"][strategy] != "undefined") { | |
// await strategies["register"][strategy](data); | |
// } | |
// } catch (err) { | |
// this.error = this.exception(err); | |
// } | |
// this.busy = false; | |
// }, | |
// async login(data = {}, strategy = "email") { | |
// data = { email: "", password: "", ...data }; | |
// this.busy = true; | |
// try { | |
// if (typeof strategies["login"][strategy] != "undefined") { | |
// await strategies["login"][strategy](data); | |
// } | |
// } catch (err) { | |
// this.error = this.exception(err); | |
// } | |
// this.busy = false; | |
// }, | |
// async logout() { | |
// return await fireAuth.signOut(auth); | |
// }, | |
// async exception(err) { | |
// return { code: "", message: "", customData: {}, name: false, ...err }; | |
// }, | |
// }, | |
// firestore: { | |
// busy: false, | |
// error: false, | |
// async save(collection, data = {}) { | |
// data = { uid: null, name: "", ...data }; | |
// const ref = fireFirestore.collection(fireFirestoreDB, collection); | |
// if (!data.uid) { | |
// const created = await fireFirestore.addDoc(ref, data); | |
// data.uid = created.id; | |
// } | |
// await fireFirestore.setDoc(fireFirestore.doc(fireFirestoreDB, collection, data.uid), data); | |
// return data; | |
// }, | |
// async find(collection, value, by = "uid") { | |
// const docRef = fireFirestore.doc(fireFirestoreDB, collection, value); | |
// const docSnap = await fireFirestore.getDoc(docRef); | |
// return docSnap.exists() ? docSnap.data() : false; | |
// }, | |
// async search(collection, query = {}) { | |
// query = { | |
// limit: 5, | |
// // orderBy: ["uid", "desc"], | |
// where: [], | |
// startAfter: false, | |
// endAt: false, | |
// ...query, | |
// }; | |
// let prev = false; | |
// let next = false; | |
// const collectRef = fireFirestore.collection(fireFirestoreDB, collection); | |
// let queryArgs = []; | |
// if (query.orderBy) { | |
// queryArgs.push(fireFirestore.orderBy.apply(null, query.orderBy)); | |
// } | |
// if (query.where.length > 0) { | |
// query.where.map((condition) => { | |
// queryArgs.push(fireFirestore.where.apply(null, condition)); | |
// }); | |
// } | |
// if (query.startAfter) { | |
// queryArgs.push( | |
// fireFirestore.startAfter( | |
// await fireFirestore.getDoc(fireFirestore.doc(fireFirestoreDB, collection, query.startAfter)) | |
// ) | |
// ); | |
// } | |
// if (query.endAt) { | |
// queryArgs.push( | |
// fireFirestore.endAt(await fireFirestore.getDoc(fireFirestore.doc(fireFirestoreDB, collection, query.endAt))) | |
// ); | |
// } | |
// if (query.limit) { | |
// queryArgs.push(fireFirestore.limit(query.limit)); | |
// } | |
// const docsQuery = fireFirestore.query.apply(null, [collectRef, ...queryArgs]); | |
// const docs = await fireFirestore.getDocs(docsQuery); | |
// let data = []; | |
// docs.forEach((doc) => { | |
// data.push({ ...doc.data(), uid: doc.id }); | |
// }); | |
// if (data.length == query.limit) { | |
// next = JSON.parse(JSON.stringify(query)); | |
// next.startAfter = data[data.length - 1]["uid"]; | |
// next.endAt = false; | |
// } | |
// return { query, data, prev, next }; | |
// }, | |
// async delete(collection, uid) {}, | |
// }, | |
// storage: { | |
// busy: false, | |
// error: false, | |
// }, | |
// events: [], | |
// on(event, callback) { | |
// this.events.push({ event, callback }); | |
// }, | |
// dispatch(eventId) { | |
// this.events.map(({ event, callback }) => { | |
// if (event != eventId) return; | |
// callback(); | |
// }); | |
// }, | |
// }); | |
// fireAuth.onAuthStateChanged(auth, (user) => { | |
// r.ready = true; | |
// if (!user) { | |
// r.user = false; | |
// r.dispatch("onAuthStateChanged"); | |
// return; | |
// } | |
// console.log("user", user); | |
// r.user = { | |
// uid: user.uid, | |
// name: (user.providerData[0] ? user.providerData[0]["displayName"] : null) || user.email, | |
// email: user.email, | |
// emailVerified: user.emailVerified, | |
// phoneNumber: user.providerData[0] ? user.providerData[0]["phoneNumber"] : "", | |
// photoURL: user.providerData[0] ? user.providerData[0]["photoURL"] : "", | |
// }; | |
// r.dispatch("onAuthStateChanged"); | |
// }); | |
// onMounted(() => { | |
// console.log(r.test); | |
// }); | |
// return r; | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { reactive } from "vue"; | |
import axios from "axios"; | |
export default (options = {}) => { | |
options = { | |
method: "get", | |
url: "", | |
params: {}, | |
data: {}, | |
autoSubmit: false, | |
response: {}, | |
validation: {}, | |
filters: {}, | |
events: {}, | |
...options, | |
}; | |
const validationRules = { | |
required: { | |
message: (value) => `Campo obrigatório`, | |
valid: (value) => !!value, | |
}, | |
email: { | |
message: () => "E-mail inválido", | |
valid: (value) => /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(value), | |
}, | |
email_exists: { | |
message: () => "E-mail já cadastrado", | |
valid: async (value) => { | |
return new Promise((resolve, reject) => { | |
setTimeout(() => { | |
resolve(true); | |
}, 1000); | |
}); | |
}, | |
}, | |
min: { | |
message: (value, min) => `Valor mínimo: ${min}`, | |
valid: (value, min) => +value >= +min, | |
}, | |
max: { | |
message: (value, max) => `Valor máximo: ${max}`, | |
valid: (value, max) => +value <= +max, | |
}, | |
numeric: { | |
message: () => "Campo não é numérico", | |
valid: (value) => !isNaN(value), | |
}, | |
}; | |
const r = reactive({ | |
...options, | |
busy: false, | |
success: false, | |
error: { message: "", fields: {} }, | |
$t: {}, | |
errorField(name) { | |
return this.error.fields[name] || []; | |
}, | |
inputBind(field) { | |
return { | |
onInput: async () => { | |
await r.fieldErrors(field); | |
}, | |
errorMessages: r.error.fields[field] || [], | |
}; | |
}, | |
filter(name, value) { | |
if ( | |
typeof options.filters[name] == "function" && | |
options.filters[name] !== null | |
) { | |
return options.filters[name](value); | |
} | |
return value; | |
}, | |
dispatch() { | |
let args = Array.from(arguments); | |
const eventName = args.shift(); | |
if ( | |
typeof this.events[eventName] == "function" && | |
this.events[eventName] !== null | |
) { | |
this.events[eventName].apply(null, args); | |
} | |
}, | |
async fieldErrors(name) { | |
this.validating = true; | |
this.timeout(name, 500, async () => { | |
const value = { ...r.params, ...r.data }[name] || ""; | |
const _call = async (call, args = []) => { | |
if (call.constructor.name == "AsyncFunction") { | |
return await call.apply(null, args); | |
} | |
return call.apply(null, args); | |
}; | |
const _errorMsg = async (name, value, rule) => { | |
let args = [value]; | |
if (typeof rule == "string") { | |
let [methodName, methodArgs = ""] = rule.split(":"); | |
const validationRule = validationRules[methodName] || null; | |
if (!validationRule) return null; | |
methodArgs = [value, ...methodArgs.split(",").filter((v) => !!v)]; | |
rule = validationRule; | |
args = methodArgs; | |
} | |
const valid = await _call(rule.valid, args); | |
const message = await _call(rule.message, args); | |
return valid ? null : message; | |
}; | |
this.error.fields[name] = ( | |
await Promise.all( | |
(options.validation[name] || []).map(async (rule) => { | |
return new Promise(async (resolve, reject) => { | |
resolve(await _errorMsg(name, value, rule)); | |
}); | |
}) | |
) | |
).filter((v) => v); | |
this.validating = false; | |
}); | |
}, | |
validating: true, | |
valid() { | |
let errors = 0; | |
if (this.validating) { | |
errors++; | |
} | |
if (this.error.message) { | |
errors++; | |
} | |
for (let i in this.error.fields) { | |
if (Array.isArray(this.error.fields[i])) { | |
errors += this.error.fields[i].length; | |
} | |
} | |
return errors == 0; | |
}, | |
async timeout(id, time, callback) { | |
if (this.$t[id]) { | |
clearTimeout(this.$t[id]); | |
} | |
this.$t[id] = setTimeout(async () => { | |
callback(); | |
delete this.$t[id]; | |
}, time); | |
}, | |
async validate() { | |
this.validating = true; | |
this.error.message = ""; | |
await Promise.all( | |
Object.entries({ ...r.data, ...r.params }).map( | |
async ([name, value]) => { | |
await r.fieldErrors(name); | |
} | |
) | |
); | |
this.validating = false; | |
}, | |
async submit() { | |
await this.validate(); | |
if (!this.valid()) return; | |
this.busy = true; | |
this.success = false; | |
try { | |
const resp = await axios({ | |
url: this.url, | |
method: this.method, | |
params: this.params, | |
data: this.data, | |
}); | |
this.dispatch("success", resp); | |
this.response = this.filter("success", resp).data; | |
this.success = true; | |
} catch (err) { | |
this.error.message = err.message; | |
this.error.fields = err.response.data.fields || {}; | |
} | |
this.busy = false; | |
}, | |
}); | |
r.validate(); | |
r.dispatch("init"); | |
if (r.autoSubmit) { | |
r.submit(); | |
} | |
return r; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export default () => { | |
return { | |
padStart(n, size = 2, str = "0") { | |
return String(n).padStart(size, str); | |
}, | |
padEnd(n, size = 2, str = "0") { | |
return String(n).padEnd(size, str); | |
}, | |
}; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* yarn add -D lodash | |
* | |
* To utiliza, insert the component "v-talk" in somewhere in the page and the | |
* functions will work with the Vuetify dialog interface. | |
* | |
* If no "v-talk" component is fond by the composable, default browser | |
* methods will be used instead. | |
* | |
* import useTalk from "@/composables/useTalk"; | |
* const talk = useTalk(); | |
* | |
* talk.alert({ text: 'Warning' }).then((resp) => console.log(resp.value)); | |
* talk.prompt({ text: 'Say my name' }).then((resp) => console.log(resp.value)); | |
* talk.confirm({ text: 'I am the danger?' }).then((resp) => console.log(resp.value)); | |
*/ | |
import { defineStore } from "pinia"; | |
import _ from "lodash"; | |
export default () => { | |
const r = defineStore("v-talk", () => { | |
const paramsDefault = (merge = {}) => { | |
merge = JSON.parse(JSON.stringify(merge)); | |
return _.merge( | |
{ | |
type: "alert", | |
action: "alert", | |
text: "", | |
value: "", | |
okText: "Ok", | |
cancelText: "Cancel", | |
inputBind: { type: "text" }, | |
}, | |
merge | |
); | |
}; | |
return reactive({ | |
shown: false, | |
params: paramsDefault(), | |
getResolveData() { | |
let params = JSON.parse(JSON.stringify(this.params)); | |
if (this.params.action == "alert") { | |
params.value = true; | |
} else if (this.params.action == "prompt") { | |
params.value = params.value ? params.value : ""; | |
} else if (this.params.action == "confirm") { | |
params.value = !!params.value; | |
} | |
return params; | |
}, | |
alert(params = {}) { | |
return this.apply({ ...params, action: "alert" }); | |
}, | |
prompt(params = {}) { | |
return this.apply({ ...params, action: "prompt" }); | |
}, | |
confirm(params = {}) { | |
return this.apply({ ...params, action: "confirm" }); | |
}, | |
apply(params) { | |
return new Promise((resolve, reject) => { | |
this.params = paramsDefault(params); | |
if (!document.querySelector(".v-talk")) { | |
if (params.action == "alert") { | |
this.params.value = true; | |
alert(this.params.text); | |
} else if (params.action == "confirm") { | |
this.params.value = confirm(this.params.text); | |
} else if (params.action == "prompt") { | |
this.params.value = prompt(this.params.text); | |
} | |
resolve(this.getResolveData()); | |
return; | |
} | |
this.shown = true; | |
setTimeout(() => { | |
const btnConfirm = document.querySelector(".v-talk-btn-confirm"); | |
const btnCancel = document.querySelector(".v-talk-btn-cancel"); | |
if (btnConfirm) { | |
btnConfirm.onclick = (ev) => { | |
if (this.params.action == "confirm") { | |
this.params.value = true; | |
} | |
resolve(this.getResolveData()); | |
this.shown = false; | |
}; | |
} | |
if (btnCancel) { | |
btnCancel.onclick = (ev) => { | |
// if (this.params.action == "confirm") { | |
// this.params.value = false; | |
// } | |
resolve(this.getResolveData()); | |
this.shown = false; | |
}; | |
} | |
}, 100); | |
}); | |
}, | |
}); | |
})(); | |
return r; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* No dependencies | |
* | |
* To add new rule, just edit "validations" object. | |
* "message" method will return error message. | |
* "handle" method will return true for valid value and false for invalid value. | |
* | |
* Use example: | |
const user = reactive({ name: '', email: '' }); | |
const v = useValidate({ | |
input: user, | |
rules: { | |
name: ["required"], | |
email: ["required", "email", "emailUnique"], | |
age: ["min:18"], | |
}, | |
customError() { | |
return { name: ['Yadda'] }; | |
}, | |
}); | |
* | |
* | |
* "v.bind" method provide all data vuetify input needs | |
* <v-text-field v-model="user.name" v-bind="v.bind('name')" /> | |
* | |
* You can write error data in HTML too: | |
* <div>{{ v.fieldError('name').join(', ') }}</div> | |
* | |
* Disabling submit button: | |
* <v-btn :disabled="v.invalid()">Save</v-btn> | |
* | |
*/ | |
import { reactive, computed } from "vue"; | |
export default (options = {}) => { | |
const validations = { | |
required: { | |
message: async (value) => `Campo obrigatório`, | |
handle: async (value) => !!value, | |
}, | |
email: { | |
message: async (value) => `E-mail inválido`, | |
handle: async (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), | |
}, | |
emailUnique: { | |
message: async (value) => `E-mail já cadastrado`, | |
handle: async (value) => { | |
return new Promise((resolve, reject) => { | |
setTimeout(() => { | |
resolve(true); | |
}, 2000); | |
}); | |
}, | |
}, | |
min: { | |
message: async (value, min) => `Valor deve ser maior que ${min}`, | |
handle: async (value, min) => parseFloat(value) >= min, | |
}, | |
max: { | |
message: async (value, max) => `Valor deve ser no máximo ${max}`, | |
handle: async (value, max) => parseFloat(value) <= max, | |
}, | |
}; | |
options = { | |
input: {}, | |
rules: {}, | |
customError: () => null, | |
...options, | |
}; | |
let _error = reactive({}); | |
const r = reactive({ | |
valid: false, | |
input: options.input, | |
rules: options.rules, | |
busy: {}, | |
focus: {}, | |
error: computed(() => { | |
let err = options.customError(); | |
if (typeof err == "object" && !Array.isArray(err)) { | |
for (let i in _error) _error[i] = []; | |
for (let i in err) _error[i] = err[i]; | |
} | |
return _error; | |
}), | |
valid() { | |
for (let field in r.input) { | |
const busy = r.busy[field] || false; | |
const error = (r.error[field] || []).length > 0; | |
const unverified = | |
(r.rules[field] || []).length > 0 && | |
false == (r.focus[field] || false); | |
if (busy || error || unverified) return false; | |
} | |
return true; | |
}, | |
invalid() { | |
return !r.valid(); | |
}, | |
fieldError(field) { | |
return r.error[field] || []; | |
}, | |
validationRuleCall(field, method, args) { | |
return new Promise((resolve, reject) => { | |
if (r.busy[field]) { | |
clearTimeout(r.busy[field]); | |
delete r.busy[field]; | |
} | |
r.busy[field] = setTimeout(async () => { | |
const valid = await validations[method]["handle"].apply(null, args); | |
const message = await validations[method]["message"].apply( | |
null, | |
args | |
); | |
resolve({ valid, message }); | |
delete r.busy[field]; | |
}, 300); | |
}); | |
}, | |
validateField(field, value) { | |
return new Promise(async (resolve, reject) => { | |
let error = []; | |
const rules = r.rules[field]; | |
for (let i in rules) { | |
let rule = rules[i]; | |
let [method, args] = rule.split(":"); | |
args = [value, ...(args || "").split(",")]; | |
const { valid, message } = await r.validationRuleCall( | |
field, | |
method, | |
args | |
); | |
if (!valid) error.push(message); | |
} | |
resolve(error); | |
}); | |
}, | |
bind(field) { | |
const value = this.input[field]; | |
if (typeof this.error[field] == "undefined") { | |
this.error[field] = []; | |
} | |
const elemHandler = async (ev) => { | |
_error[field] = await r.validateField(field, value); | |
r.focus[field] = true; | |
}; | |
return { | |
errorMessages: r.error[field] || [], | |
onKeyup: elemHandler, | |
onKeydown: elemHandler, | |
}; | |
}, | |
}); | |
return r; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* | |
* Dependencies | |
* yarn add -D @vueuse/core lodash | |
* | |
* Instantiate | |
* const vuetify = useVuetify(); | |
* | |
* Theme Switcher: | |
* vuetify.theme.switch(); // Switch between all current themes | |
* vuetify.theme.switch('dark'); // Switch to dark mode | |
* vuetify.theme.switch(['dark', 'light]); // Switch between dark or light theme. Can use more than two | |
* | |
* Theme Switcher Event | |
* vuetify.on('update:theme.name', () => {}); | |
* | |
* Display Mobile | |
* vuetify.display.mobile | |
*/ | |
import { reactive, computed } from "vue"; | |
import { useTheme, useDisplay } from "vuetify"; | |
import { useStorage } from "@vueuse/core"; | |
import { defineStore } from "pinia"; | |
import _ from "lodash"; | |
export default (options = {}) => { | |
return defineStore("vuetify", () => { | |
options = reactive( | |
_.merge( | |
{ | |
theme: { | |
name: null, | |
storageKey: "user-vuetify-theme-name", | |
config: {}, | |
mergeColors: () => ({}), | |
}, | |
}, | |
options | |
) | |
); | |
const use = { | |
theme: useTheme(), | |
display: useDisplay(), | |
}; | |
const themeStorage = useStorage( | |
options.theme.storageKey, | |
options.theme.name || use.theme.name.value | |
); | |
const r = reactive({ | |
async init() { | |
await r.theme.init(); | |
}, | |
events: [], | |
on(event, call) { | |
const exists = r.events.filter((item) => { | |
return event == item.event && call.toString() == item.call.toString(); | |
}); | |
if (exists.length) return; | |
r.events.push({ event, call }); | |
}, | |
dispatch(event, args = {}) { | |
r.events.map((evt) => { | |
if (evt.event != event) return; | |
evt.call(args); | |
}); | |
}, | |
display: { | |
mobile: computed(() => { | |
return use.display.mobile.value; | |
}), | |
}, | |
theme: { | |
async init() { | |
r.theme.switch(themeStorage.value); | |
}, | |
name: computed({ | |
get() { | |
return use.theme.name.value; | |
}, | |
set(value) { | |
use.theme.name.value = value; | |
}, | |
}), | |
config: computed(() => { | |
return use.theme.themes.value[use.theme.name.value]; | |
}), | |
themes: Object.keys(use.theme.themes.value), | |
switch(themes = null) { | |
if (themes === null) { | |
themes = Object.keys(use.theme.themes.value); | |
} | |
if (!Array.isArray(themes)) { | |
themes = [themes]; | |
} | |
let index = themes.indexOf(use.theme.name.value); | |
let indexNext = index + 1 > themes.length - 1 ? 0 : index + 1; | |
use.theme.name.value = themes[indexNext]; | |
themeStorage.value = use.theme.name.value; | |
r.dispatch("update:theme.name", { theme: use.theme.name.value }); | |
}, | |
icon(icons = {}) { | |
return icons[r.theme.name] || null; | |
}, | |
}, | |
}); | |
setTimeout(r.init, 100); | |
return r; | |
})(); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<v-app class="v-app-layout"> | |
<v-defaults-provider :defaults="defaultsProvider"> | |
<!-- loading --> | |
<v-layout v-if="!props.ready"> | |
<slot name="loading" v-bind="slotBind()"> | |
<div :class="props.loadingClass"> | |
<v-icon icon="svg-spinners:3-dots-fade" size="30" /> | |
</div> | |
</slot> | |
</v-layout> | |
<v-layout class="rounded rounded-md" v-if="props.ready"> | |
<v-navigation-drawer | |
:model-value="nav.drawer" | |
width="300" | |
elevation="0" | |
:class="`v-app-layout__navigation ${props.navigationClass}`" | |
border="0" | |
> | |
<div style="height: 100vh; overflow: auto"> | |
<slot name="navigation" v-bind="slotBind()"></slot> | |
</div> | |
</v-navigation-drawer> | |
<!-- Main --> | |
<v-main style="height: 100vh; overflow: auto; background: #7f7f7f33"> | |
<v-app-bar :class="props.headerClass" density="compact" elevation="0"> | |
<v-btn | |
icon="ci:hamburger" | |
size="30" | |
flat | |
@click="nav.drawer = !nav.drawer" | |
class="d-lg-none" | |
stacked | |
/> | |
<slot name="header" v-bind="slotBind()"></slot> | |
</v-app-bar> | |
<div class="pa-md-3"> | |
<slot name="main" v-bind="slotBind()"></slot> | |
</div> | |
</v-main> | |
</v-layout> | |
</v-defaults-provider> | |
</v-app> | |
</template> | |
<script setup> | |
import { reactive, defineProps, defineEmits, useSlots } from "vue"; | |
const props = defineProps({ | |
ready: { type: Boolean, default: true }, | |
defaultsProvider: { type: Object, default: () => ({}) }, | |
loadingClass: { | |
type: String, | |
default: "w-100 d-flex align-center justify-center", | |
}, | |
navigationClass: { type: String, default: "" }, | |
headerClass: { type: String, default: "" }, | |
mainClass: { type: String, default: "" }, | |
footerClass: { type: String, default: "" }, | |
}); | |
const slots = useSlots(); | |
const emit = defineEmits([]); | |
import { useTitle } from "@vueuse/core"; | |
const title = useTitle(); | |
import { useDisplay } from "vuetify"; | |
const display = useDisplay(); | |
import { useSwipe } from "@vueuse/core"; | |
const nav = reactive({ | |
drawer: null, | |
items: [ | |
{ title: "Home", to: "/admin" }, | |
{ title: "Pages", to: "/admin/page" }, | |
{ title: "User", to: "/admin/user" }, | |
{ title: "Test", to: "/admin/test" }, | |
], | |
}); | |
const layout = reactive({ | |
headerShow: true, | |
footerShow: true, | |
vTouch: { | |
up: () => { | |
layout.headerShow = false; | |
layout.footerShow = true; | |
}, | |
down: () => { | |
layout.headerShow = true; | |
layout.footerShow = false; | |
}, | |
}, | |
}); | |
const defaultsProvider = { | |
// VNavigationDrawer: { border: 0 }, | |
...props.defaultsProvider, | |
}; | |
const slotBind = (merge = {}) => { | |
return { | |
defaultsProvider, | |
...merge, | |
}; | |
}; | |
</script> | |
<style lang="scss"> | |
.v-app-layout { | |
&__navigation .v-navigation-drawer__content { | |
display: flex; | |
flex-direction: column; | |
} | |
} | |
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- | |
yarn add -D monaco-editor emmet | |
Monaco options: | |
https://microsoft.github.io/monaco-editor/typedoc/variables/editor.EditorOptions.html | |
--> | |
<template> | |
<div ref="editorRef" class="v-code" style="width: 100%; height: 300px"></div> | |
</template> | |
<script setup> | |
import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; | |
import "monaco-editor/esm/vs/basic-languages/html/html.contribution"; | |
import "monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution"; | |
import "monaco-editor/esm/vs/basic-languages/xml/xml.contribution"; | |
import "monaco-editor/esm/vs/basic-languages/css/css.contribution"; | |
import "monaco-editor/esm/vs/basic-languages/sql/sql.contribution"; | |
import "monaco-editor/esm/vs/basic-languages/scss/scss.contribution"; | |
import "monaco-editor/esm/vs/basic-languages/php/php.contribution"; | |
import emmet from "emmet"; | |
import { | |
ref, | |
reactive, | |
watch, | |
defineProps, | |
defineEmits, | |
onMounted, | |
nextTick, | |
} from "vue"; | |
const props = defineProps({ | |
modelValue: { | |
type: String, | |
default: "", | |
}, | |
theme: { | |
type: String, | |
default: "vs-dark", | |
}, | |
language: { | |
type: String, | |
default: "html", | |
}, | |
}); | |
const emit = defineEmits(["update:modelValue"]); | |
let editor; | |
const editorRef = ref(null); | |
const meta = reactive({ | |
height: 100, | |
}); | |
watch([props], ([propsNew]) => { | |
if (editorRef.value.contains(document.activeElement)) return; | |
if (editor) { | |
editor.setValue(propsNew.modelValue); | |
setTimeout(monacoResize, 1); | |
} | |
}); | |
const monacoResize = () => { | |
// if (editorRef.value.style.height == "100%") return; | |
// const width = editorRef.value.offsetWidth; | |
// const height = editor.getContentHeight(); | |
// editor.layout({ width, height }); | |
// editorRef.value.style.height = `${height}px`; | |
}; | |
onMounted(() => { | |
editor = monaco.editor.create(editorRef.value, { | |
value: props.modelValue, | |
wordWrap: "off", | |
minimap: { enabled: false }, | |
automaticLayout: true, | |
fontFamily: "monospace", | |
// scrollBeyondLastLine: false, | |
// wrappingStrategy: "advanced", | |
// overviewRulerLanes: 0, | |
...props, | |
}); | |
editor.onDidChangeModelContent(() => { | |
emit("update:modelValue", editor.getValue()); | |
monacoResize(); | |
}); | |
editor.addCommand(monaco.KeyCode.Tab, () => { | |
const word = editor._modelData.model | |
.getValueInRange({ | |
...editor.getSelection(), | |
startColumn: 1, | |
}) | |
.split(/[^a-zA-Z0-9]/) | |
.at(-1); | |
const pos = editor.getPosition(); | |
const text = emmet(word) || `\t`; | |
const range = new monaco.Range( | |
pos.lineNumber, | |
pos.column - word.length, | |
pos.lineNumber, | |
pos.column | |
); | |
editor.executeEdits("", [ | |
{ | |
identifier: { major: 1, minor: 1 }, | |
range, | |
text, | |
forceMoveMarkers: true, | |
}, | |
]); | |
}); | |
nextTick(async () => { | |
setTimeout(monacoResize, 1); | |
}); | |
}); | |
</script> | |
<style> | |
.v-code * { | |
font-family: monospace !important; | |
} | |
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- | |
yarn add -D @ckeditor/ckeditor5-build-classic @ckeditor/ckeditor5-vue | |
--> | |
<template> | |
<component | |
:is="CKEditor.component" | |
:editor="ClassicEditor" | |
:model-value="props.modelValue || ''" | |
@update:modelValue="emit('update:modelValue', $event)" | |
/> | |
</template> | |
<script setup> | |
import CKEditor from "@ckeditor/ckeditor5-vue"; | |
import ClassicEditor from "@ckeditor/ckeditor5-build-classic"; | |
import { reactive, defineProps, defineEmits } from "vue"; | |
const props = defineProps({ | |
modelValue: { | |
type: [String], | |
default: "", | |
}, | |
}); | |
const emit = defineEmits(["update:modelValue"]); | |
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<div class="v-talk"> | |
<v-dialog v-model="talk.shown" width="400px" v-if="props.type == 'dialog'"> | |
<v-card> | |
<v-card-text> | |
<v-text-field | |
:label="talk.params.text" | |
v-model="talk.params.value" | |
v-if="['prompt'].includes(talk.params.action)" | |
v-bind="{ ...talk.params.inputBind, hideDetails: true }" | |
/> | |
<div v-else v-html="talk.params.text"></div> | |
</v-card-text> | |
<v-card-actions> | |
<v-btn | |
class="v-talk-btn-cancel" | |
v-if="['confirm'].includes(talk.params.action)" | |
> | |
{{ talk.params.cancelText }} | |
</v-btn> | |
<v-spacer /> | |
<v-btn class="v-talk-btn-confirm bg-primary"> | |
{{ talk.params.okText }} | |
</v-btn> | |
</v-card-actions> | |
</v-card> | |
</v-dialog> | |
<v-snackbar | |
v-model="talk.shown" | |
v-bind="{ timeout: -1 }" | |
v-if="props.type == 'snackbar'" | |
> | |
<v-text-field | |
:label="talk.params.text" | |
v-model="talk.params.value" | |
v-if="['prompt'].includes(talk.params.action)" | |
v-bind="{ ...talk.params.inputBind, hideDetails: true }" | |
class="mt-3" | |
/> | |
<div v-else v-html="talk.params.text"></div> | |
<template #actions> | |
<v-btn | |
class="v-talk-btn-cancel" | |
v-if="['confirm'].includes(talk.params.action)" | |
> | |
{{ talk.params.cancelText }} | |
</v-btn> | |
<v-btn class="v-talk-btn-confirm bg-primary"> | |
{{ talk.params.okText }} | |
</v-btn> | |
</template> | |
</v-snackbar> | |
</div> | |
</template> | |
<script setup> | |
import { reactive, defineProps, defineEmits } from "vue"; | |
const props = defineProps({ | |
type: { type: String, default: "dialog" }, // dialog, snackbar | |
}); | |
import useTalk from "@/composables/useTalk"; | |
const talk = useTalk(); | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment