Migrate front-end to Nuxt app (#284)
* wip * Managed to load a page * Stuck at changing routes * Fixed the router, and editable div * WIP * Fix app loader * WIP * Fix check-auth middleware * Started to refactor input components * WIP * Added select input, v-click-outside for vselect * update vselect & phone input * Fixed the mixin * input component updates * Fix signature input import * input component updates in vue3 * image input in vue3 * small fixes * fix useFormInput watcher * scale input in vue3 * Vue3: migrating from vuex to Pinia (#249) * Vue3: migrating from vuex to Pinia * toggle input fixes * update configureCompat --------- Co-authored-by: Forms Dev <chirag+new@notionforms.io> * support vue3 query builder * Refactor inpus * fix: Vue3 Query Builder - Logic Editor (#251) * support vue3 query builder * upgrade * remove local from middleware * Submission table pagination & migrate chart to vue3 (#254) * Submission table Pagination in background * migrate chart to vue3 * Form submissions pagination * Form submissions * Fix form starts * Fix openSelect key issue --------- Co-authored-by: Forms Dev <chirag+new@notionforms.io> Co-authored-by: Julien Nahum <julien@nahum.net> * Vue 3 better animation (#257) * vue-3-better-animation * Working on migration to vueuse/motion * Form sidebar animations * Clean code * Added animations for modal * Finished implementing better animations --------- Co-authored-by: Forms Dev <chirag+new@notionforms.io> * Work in progress * Migrating amplitude and crisp plugin/composable * Started to refactor pages * WIP * vue3-scroll-shadow-fixes (#260) * WIP * WIP * WIP * Figured out auth & middlewares * WI * Refactoring stores and templates pages to comp. api * Finishing the templates pages * fix collapsible * Finish reworking most templates pages * Reworked workspaces store * Working on home page and modal * Fix dropdown * Fix modal * Fixed form creation * Fixed most of the form/show pages * Updated cors dependency * fix custom domain warning * NuxtLink migration (#262) Co-authored-by: Forms Dev <chirag+new@notionforms.io> * Tiny fixes + start pre-rendering * migrate-to-nuxt-useappconfig (#263) * migrate-to-nuxt-useappconfig * defineAppConfig --------- Co-authored-by: Forms Dev <chirag+new@notionforms.io> * Working on form/show and editor * Globally import form inputs to fix resolve * Remove vform - working on form public page * Remove initform mixin * Work in progress for form create guess user * Nuxt Migration notifications (#265) * Nuxt Migration notifications * @input to @update:model-value * change field type fixes * @update:model-value * Enable form-block-logic-editor * vue-confetti migration * PR request changes * useAlert in setup * Migrate to nuxt settings page AND remove axios (#266) * Settings pages migration * remove axios and use opnFetch * Make created form reactive (#267) * Remove verify pages and axios lib --------- Co-authored-by: Julien Nahum <julien@nahum.net> * Fix alert styling + bug fixes and cleaning * Refactor notifications + add shadow * Fix vselect issue * Working on page pre-rendering * Created NotionPages store * Added sitemap on nuxt side * Sitemap done, working on aws amplify * Adding missing module * Remove axios and commit backend changes to sitemap * Fix notifications * fix guestpage editor (#269) Co-authored-by: Julien Nahum <julien@nahum.net> * Remove appconfig in favor of runtimeconfig * Fixed amplitude bugs, and added staging environment * Added amplify file * Change basdirectory amplify * Fix loading bar position * Fix custom redirect (#273) * Dirty form handling - nuxt migration (#272) * SEO meta nuxt migration (#274) * SEO meta nuxt migration * Polish seo metas, add defaults for OG and twitter --------- Co-authored-by: Julien Nahum <julien@nahum.net> * migrate to nuxt useClipboard (#268) * Set middleware on pages (#278) * Se middleware on pages * Se middleware on account page * add robots.txt (#276) * 404 page migration (#277) * Templates pages migration (#275) * NuxtImg Migration (#279) Co-authored-by: Julien Nahum <julien@nahum.net> * Update package json * Fix build script * Add loglevel param * Disable page pre-rendering * Attempt to allow svgs * Fix SVGs with NuxtImage * Add .env file at AWS build time * tRGIGGER deploy * Fix issue * ANother attrempt * Fix typo * Fix env? * Attempt to simplify build * Enable swr caching instead of prerenderign * Better image compression * Last attempt at nuxt images efficiency * Improve image optimization again * Remove NuxtImg for non asset files * Restore templates pages cache * Remove useless images + fix templates show page * image optimization caching + fix hydratation issue form template page * URL generation (front&back) + fixed authJWT for SSR * Fix composable issue * Fix form share page * Embeddable form as a nuxt middleware * Fix URL for embeddable middleware * Debugging embeddable on amplify * Add custom domain support * No follow for non-production env * Fix sentry nuxt and custom domain redirect * remove api prefix from routes (#280) * remove api prefix from routes * PR changes --------- Co-authored-by: Julien Nahum <julien@nahum.net> * nuxt migration -file upload - WIP (#271) Co-authored-by: Julien Nahum <julien@nahum.net> * Fix local file upload * Fix file submissions preview * API redirect to back-end from nuxt * API redirect to back-end from nuxt * Remove old JS app, update deploy script * Fix tests, added gh action nuxt step * Updated package-lock.json * Setup node in GH Nuxt action * Setup client directory for GH workflow --------- Co-authored-by: Forms Dev <chirag+new@notionforms.io> Co-authored-by: Chirag Chhatrala <60499540+chiragchhatrala@users.noreply.github.com> Co-authored-by: Rishi Raj Jain <rishi18304@iiitd.ac.in> Co-authored-by: formsdev <136701234+formsdev@users.noreply.github.com>
This commit is contained in:
54
client/composables/forms/initForm.js
vendored
Normal file
54
client/composables/forms/initForm.js
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
|
||||
export const initForm = (defaultValue = {}) => {
|
||||
return useForm({
|
||||
title: 'My Form',
|
||||
description: null,
|
||||
visibility: 'public',
|
||||
workspace_id: null,
|
||||
properties: [],
|
||||
|
||||
notifies: false,
|
||||
slack_notifies: false,
|
||||
send_submission_confirmation: false,
|
||||
webhook_url: null,
|
||||
notification_settings: {},
|
||||
|
||||
// Customization
|
||||
theme: 'default',
|
||||
width: 'centered',
|
||||
dark_mode: 'auto',
|
||||
color: '#3B82F6',
|
||||
hide_title: false,
|
||||
no_branding: false,
|
||||
uppercase_labels: true,
|
||||
transparent_background: false,
|
||||
closes_at: null,
|
||||
closed_text: 'This form has now been closed by its owner and does not accept submissions anymore.',
|
||||
auto_save: true,
|
||||
|
||||
// Submission
|
||||
submit_button_text: 'Submit',
|
||||
re_fillable: false,
|
||||
re_fill_button_text: 'Fill Again',
|
||||
submitted_text: 'Amazing, we saved your answers. Thank you for your time and have a great day!',
|
||||
notification_sender: 'OpnForm',
|
||||
notification_subject: 'We saved your answers',
|
||||
notification_body: 'Hello there 👋 <br>This is a confirmation that your submission was successfully saved.',
|
||||
notifications_include_submission: true,
|
||||
use_captcha: false,
|
||||
is_rating: false,
|
||||
rating_max_value: 5,
|
||||
max_submissions_count: null,
|
||||
max_submissions_reached_text: 'This form has now reached the maximum number of allowed submissions and is now closed.',
|
||||
editable_submissions_button_text: 'Edit submission',
|
||||
confetti_on_submission: false,
|
||||
|
||||
// Security & Privacy
|
||||
can_be_indexed: true,
|
||||
|
||||
// Custom SEO
|
||||
seo_meta: {},
|
||||
|
||||
...defaultValue
|
||||
})
|
||||
}
|
||||
34
client/composables/forms/pendingSubmission.js
vendored
Normal file
34
client/composables/forms/pendingSubmission.js
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
import {hash} from "~/lib/utils.js"
|
||||
import {useStorage} from "@vueuse/core"
|
||||
|
||||
export const pendingSubmission = (form) => {
|
||||
|
||||
const formPendingSubmissionKey = computed(() => {
|
||||
return (form) ? form.form_pending_submission_key + '-' + hash(window.location.href) : ''
|
||||
})
|
||||
|
||||
const enabled = computed(() => {
|
||||
return form?.auto_save ?? false
|
||||
})
|
||||
|
||||
const set = (value) => {
|
||||
if (process.server || !enabled.value) return
|
||||
useStorage(formPendingSubmissionKey.value).value = JSON.stringify(value)
|
||||
}
|
||||
|
||||
const remove = () => {
|
||||
return set(null)
|
||||
}
|
||||
|
||||
const get = (defaultValue = {}) => {
|
||||
if (process.server || !enabled.value) return
|
||||
const pendingSubmission = useStorage(formPendingSubmissionKey.value).value
|
||||
return pendingSubmission ? JSON.parse(pendingSubmission) : defaultValue
|
||||
}
|
||||
|
||||
return {
|
||||
enabled,
|
||||
set,
|
||||
get
|
||||
}
|
||||
}
|
||||
14
client/composables/forms/validatePropertiesLogic.js
vendored
Normal file
14
client/composables/forms/validatePropertiesLogic.js
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
import FormPropertyLogicRule from "~/lib/forms/FormPropertyLogicRule.js";
|
||||
|
||||
export const validatePropertiesLogic = (properties) => {
|
||||
properties.forEach((field) => {
|
||||
const isValid = (new FormPropertyLogicRule(field)).isValid()
|
||||
if (!isValid) {
|
||||
field.logic = {
|
||||
conditions: null,
|
||||
actions: []
|
||||
}
|
||||
}
|
||||
})
|
||||
return properties
|
||||
}
|
||||
75
client/composables/lib/vForm/Errors.js
vendored
Normal file
75
client/composables/lib/vForm/Errors.js
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
|
||||
function arrayWrap(value) {
|
||||
return Array.isArray(value) ? value : [value];
|
||||
}
|
||||
|
||||
export default class Errors {
|
||||
constructor() {
|
||||
this.errors = {};
|
||||
}
|
||||
|
||||
set(field, messages = undefined) {
|
||||
if (typeof field === 'object') {
|
||||
this.errors = field;
|
||||
} else {
|
||||
this.set({ ...this.errors, [field]: arrayWrap(messages) });
|
||||
}
|
||||
}
|
||||
|
||||
all() {
|
||||
return this.errors;
|
||||
}
|
||||
|
||||
has(field) {
|
||||
return Object.prototype.hasOwnProperty.call(this.errors, field);
|
||||
}
|
||||
|
||||
hasAny(...fields) {
|
||||
return fields.some(field => this.has(field));
|
||||
}
|
||||
|
||||
any() {
|
||||
return Object.keys(this.errors).length > 0;
|
||||
}
|
||||
|
||||
get(field) {
|
||||
if (this.has(field)) {
|
||||
return this.getAll(field)[0];
|
||||
}
|
||||
}
|
||||
|
||||
getAll(field) {
|
||||
return arrayWrap(this.errors[field] || []);
|
||||
}
|
||||
|
||||
only(...fields) {
|
||||
const messages = [];
|
||||
|
||||
fields.forEach((field) => {
|
||||
const message = this.get(field);
|
||||
if (message) {
|
||||
messages.push(message);
|
||||
}
|
||||
});
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
flatten() {
|
||||
return Object.values(this.errors).reduce((a, b) => a.concat(b), []);
|
||||
}
|
||||
|
||||
clear(field = undefined) {
|
||||
const errors = {};
|
||||
|
||||
if (field) {
|
||||
Object.keys(this.errors).forEach((key) => {
|
||||
if (key !== field) {
|
||||
errors[key] = this.errors[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.set(errors);
|
||||
}
|
||||
}
|
||||
170
client/composables/lib/vForm/Form.js
vendored
Normal file
170
client/composables/lib/vForm/Form.js
vendored
Normal file
@@ -0,0 +1,170 @@
|
||||
import {serialize} from 'object-to-formdata';
|
||||
import Errors from './Errors';
|
||||
import cloneDeep from 'clone-deep';
|
||||
import {opnFetch} from "~/composables/useOpnApi.js";
|
||||
function hasFiles(data) {
|
||||
return data instanceof File ||
|
||||
data instanceof Blob ||
|
||||
data instanceof FileList ||
|
||||
(typeof data === 'object' && data !== null && Object.values(data).find(value => hasFiles(value)) !== undefined);
|
||||
}
|
||||
|
||||
class Form {
|
||||
constructor(data = {}) {
|
||||
this.originalData = {};
|
||||
this.busy = false;
|
||||
this.successful = false;
|
||||
this.recentlySuccessful = false;
|
||||
this.recentlySuccessfulTimeoutId = undefined;
|
||||
this.errors = new Errors();
|
||||
this.update(data);
|
||||
}
|
||||
|
||||
static errorMessage = 'Something went wrong. Please try again.';
|
||||
static recentlySuccessfulTimeout = 2000;
|
||||
static ignore = ['busy', 'successful', 'errors', 'originalData', 'recentlySuccessful', 'recentlySuccessfulTimeoutId'];
|
||||
|
||||
static make(augment) {
|
||||
return new this(augment);
|
||||
}
|
||||
|
||||
update(data) {
|
||||
this.originalData = Object.assign({}, this.originalData, cloneDeep(data));
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
fill(data = {}) {
|
||||
this.keys().forEach((key) => {
|
||||
this[key] = data[key];
|
||||
});
|
||||
}
|
||||
|
||||
data() {
|
||||
return this.keys().reduce((data, key) => (
|
||||
{...data, [key]: this[key]}
|
||||
), {});
|
||||
}
|
||||
|
||||
keys() {
|
||||
return Object.keys(this).filter(key => !Form.ignore.includes(key));
|
||||
}
|
||||
|
||||
startProcessing() {
|
||||
this.errors.clear();
|
||||
this.busy = true;
|
||||
this.successful = false;
|
||||
this.recentlySuccessful = false;
|
||||
clearTimeout(this.recentlySuccessfulTimeoutId);
|
||||
}
|
||||
|
||||
finishProcessing() {
|
||||
this.busy = false;
|
||||
this.successful = true;
|
||||
this.recentlySuccessful = true;
|
||||
this.recentlySuccessfulTimeoutId = setTimeout(() => {
|
||||
this.recentlySuccessful = false;
|
||||
}, Form.recentlySuccessfulTimeout);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.errors.clear();
|
||||
this.successful = false;
|
||||
this.recentlySuccessful = false;
|
||||
clearTimeout(this.recentlySuccessfulTimeoutId);
|
||||
}
|
||||
|
||||
reset() {
|
||||
Object.keys(this)
|
||||
.filter(key => !Form.ignore.includes(key))
|
||||
.forEach((key) => {
|
||||
this[key] = JSON.parse(JSON.stringify(this.originalData[key]));
|
||||
});
|
||||
}
|
||||
|
||||
get(url, config = {}) {
|
||||
return this.submit('get', url, config);
|
||||
}
|
||||
|
||||
post(url, config = {}) {
|
||||
return this.submit('post', url, config);
|
||||
}
|
||||
|
||||
patch(url, config = {}) {
|
||||
return this.submit('patch', url, config);
|
||||
}
|
||||
|
||||
put(url, config = {}) {
|
||||
return this.submit('put', url, config);
|
||||
}
|
||||
|
||||
delete(url, config = {}) {
|
||||
return this.submit('delete', url, config);
|
||||
}
|
||||
|
||||
submit(method, url, config = {}) {
|
||||
this.startProcessing();
|
||||
|
||||
config = {
|
||||
body: {},
|
||||
params: {},
|
||||
url: url,
|
||||
method: method,
|
||||
...config
|
||||
};
|
||||
|
||||
if (method.toLowerCase() === 'get') {
|
||||
config.params = {...this.data(), ...config.params};
|
||||
} else {
|
||||
config.body = {...this.data(), ...config.data};
|
||||
|
||||
if (hasFiles(config.data) && !config.transformRequest) {
|
||||
config.transformRequest = [data => serialize(data)];
|
||||
}
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
opnFetch(config.url, config)
|
||||
.then((data) => {
|
||||
this.finishProcessing();
|
||||
resolve(data);
|
||||
}).catch((error) => {
|
||||
this.handleErrors(error);
|
||||
reject(error)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
handleErrors(error) {
|
||||
this.busy = false;
|
||||
|
||||
if (error) {
|
||||
this.errors.set(this.extractErrors(error.data));
|
||||
}
|
||||
}
|
||||
|
||||
extractErrors(data) {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return {error: Form.errorMessage};
|
||||
|
||||
}
|
||||
|
||||
if (data.errors) {
|
||||
return {...data.errors};
|
||||
}
|
||||
|
||||
if (data.message) {
|
||||
return {error: data.message};
|
||||
}
|
||||
|
||||
return {...data};
|
||||
}
|
||||
|
||||
onKeydown(event) {
|
||||
const target = event.target;
|
||||
|
||||
if (target.name) {
|
||||
this.errors.clear(target.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Form;
|
||||
65
client/composables/stores/useContentStore.js
vendored
Normal file
65
client/composables/stores/useContentStore.js
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
|
||||
// Composable with all the logic to encapsulate a default content store
|
||||
|
||||
export const useContentStore = (mapKey = 'id') => {
|
||||
|
||||
const content = ref(new Map())
|
||||
const loading = ref(false)
|
||||
|
||||
// Computed
|
||||
const getAll = computed(() => {
|
||||
return [...content.value.values()]
|
||||
})
|
||||
const getByKey = (key) => {
|
||||
if (Array.isArray(key)) {
|
||||
return key.map((k) => content.value.get(k)).filter((item) => item !== undefined)
|
||||
}
|
||||
return content.value.get(key)
|
||||
}
|
||||
|
||||
const length = computed(() => content.value.size)
|
||||
|
||||
// Actions
|
||||
function set(items) {
|
||||
content.value = new Map
|
||||
save(items)
|
||||
}
|
||||
|
||||
function save(items) {
|
||||
if (!items) return
|
||||
if (!Array.isArray(items)) items = [items]
|
||||
items.forEach((item) => {
|
||||
content.value.set(item[mapKey], item)
|
||||
})
|
||||
}
|
||||
function remove(item) {
|
||||
content.value.delete( typeof item === 'object' ? item[mapKey] : item)
|
||||
}
|
||||
|
||||
function startLoading() {
|
||||
loading.value = true
|
||||
}
|
||||
|
||||
function stopLoading() {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
set([])
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
return {
|
||||
content,
|
||||
loading,
|
||||
getAll,
|
||||
getByKey,
|
||||
length,
|
||||
set,
|
||||
save,
|
||||
remove,
|
||||
startLoading,
|
||||
stopLoading,
|
||||
resetState
|
||||
}
|
||||
}
|
||||
51
client/composables/useAlert.js
vendored
Normal file
51
client/composables/useAlert.js
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
const { notify } = useNotification()
|
||||
|
||||
export const useAlert = () => {
|
||||
|
||||
function success(message, autoClose = 10000) {
|
||||
notify({
|
||||
title: 'Success',
|
||||
text: message,
|
||||
type: 'success',
|
||||
duration: autoClose
|
||||
})
|
||||
}
|
||||
|
||||
function error(message, autoClose = 10000) {
|
||||
notify({
|
||||
title: 'Error',
|
||||
text: message,
|
||||
type: 'error',
|
||||
duration: autoClose
|
||||
})
|
||||
}
|
||||
|
||||
function warning(message, autoClose = 10000) {
|
||||
notify({
|
||||
title: 'Warning',
|
||||
text: message,
|
||||
type: 'warning',
|
||||
duration: autoClose
|
||||
})
|
||||
}
|
||||
|
||||
function confirm(message, success, failure = ()=>{}, autoClose = 10000) {
|
||||
notify({
|
||||
title: 'Confirm',
|
||||
text: message,
|
||||
type: 'confirm',
|
||||
duration: autoClose,
|
||||
data: {
|
||||
success,
|
||||
failure
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
success,
|
||||
error,
|
||||
warning,
|
||||
confirm
|
||||
}
|
||||
}
|
||||
40
client/composables/useAmplitude.js
vendored
Normal file
40
client/composables/useAmplitude.js
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
import amplitude from 'amplitude-js'
|
||||
|
||||
export const useAmplitude = () => {
|
||||
const config = useRuntimeConfig()
|
||||
const amplitudeCode = config.public.amplitudeCode
|
||||
const amplitudeClient = amplitudeCode ? amplitude.getInstance() : null;
|
||||
if (amplitudeClient) {
|
||||
amplitudeClient.init(amplitudeCode)
|
||||
}
|
||||
|
||||
const logEvent = function (eventName, eventData) {
|
||||
if (!config.public.env === 'production' || !amplitudeClient) {
|
||||
console.log('[DEBUG] Amplitude logged event:', eventName, eventData)
|
||||
return
|
||||
}
|
||||
|
||||
if (eventData && typeof eventData !== 'object') {
|
||||
throw new Error('Amplitude event value must be an object.')
|
||||
}
|
||||
|
||||
amplitudeClient.logEvent(eventName, eventData)
|
||||
}
|
||||
|
||||
const setUser = function (user) {
|
||||
if (!amplitudeClient) return
|
||||
amplitudeClient.setUserId(user.id)
|
||||
amplitudeClient.setUserProperties({
|
||||
email: user.email,
|
||||
subscribed: user.is_subscribed,
|
||||
enterprise_subscription: user.has_enterprise_subscription
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
logEvent,
|
||||
setUser,
|
||||
amplitude: amplitudeClient
|
||||
}
|
||||
}
|
||||
22
client/composables/useConfetti.js
vendored
Normal file
22
client/composables/useConfetti.js
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
|
||||
export const useConfetti = () => {
|
||||
let timeoutId = ref(null)
|
||||
const nuxtApp = useNuxtApp()
|
||||
const $confetti = nuxtApp.vueApp.config.globalProperties.$confetti
|
||||
|
||||
function play(duration=3000) {
|
||||
$confetti.start({ defaultSize: 6 })
|
||||
timeoutId = setTimeout(() => {
|
||||
$confetti.stop()
|
||||
}, duration)
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeoutId) clearTimeout(timeoutId)
|
||||
})
|
||||
|
||||
return {
|
||||
play
|
||||
}
|
||||
}
|
||||
88
client/composables/useCrisp.js
vendored
Normal file
88
client/composables/useCrisp.js
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
export const useCrisp = () => {
|
||||
|
||||
let crisp = process.client ? window.Crisp : null
|
||||
|
||||
function openChat() {
|
||||
if (!crisp) return
|
||||
showChat()
|
||||
crisp.chat.open()
|
||||
}
|
||||
|
||||
function showChat() {
|
||||
if (!crisp) return
|
||||
crisp.chat.show()
|
||||
}
|
||||
|
||||
function hideChat() {
|
||||
if (!crisp) return
|
||||
crisp.chat.hide()
|
||||
}
|
||||
|
||||
function closeChat() {
|
||||
if (!crisp) return
|
||||
crisp.chat.close()
|
||||
}
|
||||
|
||||
function openAndShowChat(message = null) {
|
||||
if (!crisp) return
|
||||
showChat()
|
||||
openChat()
|
||||
if (message) sendTextMessage(message)
|
||||
}
|
||||
|
||||
function openHelpdesk() {
|
||||
if (!crisp) return
|
||||
openChat()
|
||||
crisp.chat.setHelpdeskView()
|
||||
}
|
||||
|
||||
function openHelpdeskArticle(articleSlug, locale = 'en') {
|
||||
if (!crisp) return
|
||||
crisp.chat.openHelpdeskArticle(locale, articleSlug);
|
||||
}
|
||||
|
||||
function sendTextMessage(message) {
|
||||
if (!crisp) return
|
||||
crisp.message.send('text', message)
|
||||
}
|
||||
|
||||
function setUser(user) {
|
||||
if (!crisp) return
|
||||
crisp.user.setEmail(user.email);
|
||||
crisp.user.setNickname(user.name);
|
||||
crisp.session.setData({
|
||||
user_id: user.id,
|
||||
'pro-subscription': user?.is_subscribed ?? false,
|
||||
'stripe-id': user?.stripe_id ?? '',
|
||||
'subscription': user?.has_enterprise_subscription ? 'enterprise' : 'pro'
|
||||
});
|
||||
|
||||
if (user?.is_subscribed ?? false) {
|
||||
setSegments(['subscribed', user?.has_enterprise_subscription ? 'enterprise' : 'pro'])
|
||||
}
|
||||
}
|
||||
|
||||
function pushEvent(event, data = {}) {
|
||||
if (!crisp) return
|
||||
crisp.pushEvent(event, data)
|
||||
}
|
||||
|
||||
function setSegments(segments, overwrite = false) {
|
||||
if (!crisp) return
|
||||
crisp.session.setSegments(segments, overwrite)
|
||||
}
|
||||
|
||||
return {
|
||||
crisp,
|
||||
openChat,
|
||||
showChat,
|
||||
hideChat,
|
||||
closeChat,
|
||||
openAndShowChat,
|
||||
openHelpdesk,
|
||||
openHelpdeskArticle,
|
||||
sendTextMessage,
|
||||
pushEvent,
|
||||
setUser
|
||||
}
|
||||
}
|
||||
5
client/composables/useForm.js
vendored
Normal file
5
client/composables/useForm.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import Form from "~/composables/lib/vForm/Form.js"
|
||||
|
||||
export const useForm = (formData = {}) => {
|
||||
return reactive(new Form(formData))
|
||||
}
|
||||
6
client/composables/useIsIframe.js
vendored
Normal file
6
client/composables/useIsIframe.js
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
export const useIsIframe = () => {
|
||||
if (process.client) {
|
||||
return window.location !== window.parent.location || window.frameElement
|
||||
}
|
||||
return false
|
||||
}
|
||||
80
client/composables/useOpnApi.js
vendored
Normal file
80
client/composables/useOpnApi.js
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
import {getDomain, getHost, customDomainUsed} from "~/lib/utils.js";
|
||||
|
||||
function addAuthHeader(request, options) {
|
||||
const authStore = useAuthStore()
|
||||
if (authStore.token) {
|
||||
options.headers = {Authorization: `Bearer ${authStore.token}`, ...options.headers}
|
||||
}
|
||||
}
|
||||
|
||||
function addPasswordToFormRequest(request, options) {
|
||||
const url = request.url
|
||||
if (!url || !url.startsWith('/forms/')) return
|
||||
|
||||
const slug = url.split('/')[3]
|
||||
const passwordCookie = useCookie('password-' + slug, {maxAge: 60 * 60 * 24 * 30}) // 30 days
|
||||
if (slug !== undefined && slug !== '' && passwordCookie.value !== undefined) {
|
||||
options.headers['form-password'] = passwordCookie.value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom domain header if custom domain is used
|
||||
*/
|
||||
function addCustomDomainHeader(request, options) {
|
||||
if (!customDomainUsed()) return
|
||||
options.headers['x-custom-domain'] = getDomain(getHost())
|
||||
}
|
||||
|
||||
export function getOpnRequestsOptions(request, opts) {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
if (opts.body && opts.body instanceof FormData) {
|
||||
opts.headers = {
|
||||
'charset': 'utf-8',
|
||||
...opts.headers,
|
||||
}
|
||||
}
|
||||
|
||||
opts.headers = {accept: 'application/json', ...opts.headers}
|
||||
|
||||
// Authenticate requests coming from the server
|
||||
if (process.server && config.apiSecret) {
|
||||
opts.headers['x-api-secret'] = config.apiSecret
|
||||
}
|
||||
|
||||
addAuthHeader(request, opts)
|
||||
addPasswordToFormRequest(request, opts)
|
||||
addCustomDomainHeader(request, opts)
|
||||
|
||||
if (!opts.baseURL) opts.baseURL = config.public.apiBase
|
||||
|
||||
return {
|
||||
async onResponseError({response}) {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const {status} = response
|
||||
if (status === 401) {
|
||||
if (authStore.check) {
|
||||
console.log("Logging out due to 401")
|
||||
authStore.logout()
|
||||
useRouter().push({name: 'login'})
|
||||
}
|
||||
} else if (status === 420) {
|
||||
// If invalid domain, redirect to main domain
|
||||
window.location.href = config.public.appUrl + '?utm_source=failed_custom_domain_redirect'
|
||||
} else if (status >= 500) {
|
||||
console.error('Request error', status)
|
||||
}
|
||||
},
|
||||
...opts
|
||||
}
|
||||
}
|
||||
|
||||
export const opnFetch = (request, opts = {}) => {
|
||||
return $fetch(request, getOpnRequestsOptions(request, opts))
|
||||
}
|
||||
|
||||
export const useOpnApi = (request, opts = {}) => {
|
||||
return useFetch(request, getOpnRequestsOptions(request, opts))
|
||||
}
|
||||
16
client/composables/useOpnSeoMeta.js
vendored
Normal file
16
client/composables/useOpnSeoMeta.js
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
export const useOpnSeoMeta = (meta) => {
|
||||
return useSeoMeta({
|
||||
...meta.title ? {
|
||||
ogTitle: meta.title,
|
||||
twitterTitle: meta.title,
|
||||
} : {},
|
||||
...meta.description ? {
|
||||
ogDescription: meta.description,
|
||||
twitterDescription: meta.description,
|
||||
} : {},
|
||||
...meta.ogImage ? {
|
||||
twitterImage: meta.ogImage,
|
||||
} : {},
|
||||
...meta,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user