Skip to content

Instantly share code, notes, and snippets.

@jeff-silva
Last active January 6, 2024 23:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jeff-silva/f48fcbfc7a166f1d77018b78b7271d82 to your computer and use it in GitHub Desktop.
Save jeff-silva/f48fcbfc7a166f1d77018b78b7271d82 to your computer and use it in GitHub Desktop.
Vue3 Helpers
/**
* 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;
};
/**
* 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;
});
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;
};
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);
},
};
};
/**
* 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;
};
/**
* 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;
};
/**
*
* 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;
})();
};
<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>
<!--
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>
<!--
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>
<template>
<v-list v-if="props.deep == 0" density="compact">
<v-nav :items="items" :deep="props.deep + 1" />
</v-list>
<template v-else>
<template v-for="(_item, i) in items">
<template v-if="!isObject(_item)">
<v-divider />
</template>
<template v-else-if="_item.children.length > 0">
<v-list-group>
<template #activator="bind">
<v-list-item v-bind="{ ...bind.props, ..._item.bind }">
{{ _item.title }}
</v-list-item>
</template>
<v-nav :items="_item.children" :deep="props.deep + 1" />
</v-list-group>
</template>
<template v-else>
<v-list-item v-if="_item.condition(_item)" v-bind="_item.bind">
{{ _item.title }}
</v-list-item>
</template>
</template>
</template>
</template>
<script setup>
import { defineProps, computed } from "vue";
import _ from "lodash";
const props = defineProps({
items: { type: Array, default: () => [] },
deep: { type: Number, default: 0 },
});
const isObject = (item) => {
return _.isPlainObject(item);
};
const getItem = (item) => {
if (!isObject(item)) return item;
item = {
title: "",
to: null,
icon: null,
bind: {},
condition: () => true,
children: [],
...item,
};
if (item.to) item.bind.to = item.to;
if (item.icon) item.bind.prependIcon = item.icon;
return item;
};
const items = computed(() => props.items.map(getItem));
</script>
<style>
/* .v-list-item__prepend {
max-width: 40px;
} */
/* .v-list-item {
padding-inline-start: 0 !important;
} */
</style>
<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