Merge branch 'main' into vue-3

This commit is contained in:
Forms Dev
2023-11-28 16:24:36 +05:30
81 changed files with 2327 additions and 1494 deletions

View File

@@ -43,25 +43,17 @@
Log In
</router-link>
</p>
<!-- GitHub Register Button -->
<login-with-github />
</form>
</div>
</template>
<script>
import Form from 'vform'
import LoginWithGithub from '~/components/LoginWithGithub.vue'
import SelectInput from '../../../components/forms/SelectInput.vue'
import { initCrisp } from '../../../middleware/check-auth.js'
export default {
name: 'RegisterForm',
components: {
SelectInput,
LoginWithGithub
},
components: {},
props: {
isQuick: {
type: Boolean,
@@ -76,7 +68,8 @@ export default {
email: '',
password: '',
password_confirmation: '',
agree_terms: false
agree_terms: false,
appsumo_license: null
}),
mustVerifyEmail: false
}),
@@ -99,6 +92,13 @@ export default {
}
},
mounted () {
// Set appsumo license
if (this.$route.query.appsumo_license !== undefined && this.$route.query.appsumo_license) {
this.form.appsumo_license = this.$route.query.appsumo_license
}
},
methods: {
async register () {
// Register the user.
@@ -123,6 +123,15 @@ export default {
initCrisp(data)
this.$crisp.push(['set', 'session:event', [[['register', {}, 'blue']]]])
// AppSumo License
if (data.appsumo_license === false) {
this.alertError('Invalid AppSumo license. This probably happened because this license was already' +
' attached to another OpnForm account. Please contact support.')
} else if (data.appsumo_license === true) {
this.alertSuccess('Your AppSumo license was successfully activated! You now have access to all the' +
' features of the AppSumo deal.')
}
// Redirect
if (this.isQuick) {
this.$emit('afterQuickLogin')

View File

@@ -3,16 +3,17 @@
<div class="flex mt-6 mb-10">
<div class="w-full md:max-w-6xl mx-auto px-4 flex items-center md:flex-row-reverse flex-wrap">
<div class="w-full lg:w-1/2 md:p-6">
<app-sumo-register class="mb-10 p-6 lg:hidden" />
<div class="border rounded-md p-6 shadow-md sticky top-4">
<h2 class="font-semibold text-2xl">
Create an account
</h2>
<small>Sign up in less than 2 minutes.</small>
<register-form />
</div>
</div>
<div class="w-full hidden lg:block lg:w-1/2 md:p-6 mt-8 md:mt-0 ">
<app-sumo-register class="mb-10" />
<h1 class="font-bold">
Create beautiful forms and share them anywhere
</h1>
@@ -22,7 +23,7 @@
<div class="flex flex-wrap justify-center">
<p class="px-3 pb-3 text-sm text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2"
stroke="currentColor" stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
@@ -30,7 +31,7 @@
</p>
<p class="px-3 pb-3 text-sm text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2"
stroke="currentColor" stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
@@ -38,16 +39,16 @@
</p>
<p class="px-3 pb-3 text-sm text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2"
stroke="currentColor" stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
Unlimited submissions
</p>
</div>
<!-- <div class="mt-3 p-6">-->
<!-- <testimonials />-->
<!-- </div>-->
<!-- <div class="mt-3 p-6">-->
<!-- <testimonials />-->
<!-- </div>-->
</div>
</div>
</div>
@@ -57,29 +58,27 @@
<script>
import OpenFormFooter from '../../components/pages/OpenFormFooter.vue'
import Testimonials from '../../components/pages/welcome/Testimonials.vue'
import RegisterForm from './components/RegisterForm.vue'
import SeoMeta from '../../mixins/seo-meta.js'
import AppSumoRegister from '../../components/vendor/appsumo/AppSumoRegister.vue'
export default {
components: {
Testimonials,
AppSumoRegister,
OpenFormFooter,
RegisterForm
},
middleware: 'guest',
mixins: [SeoMeta],
middleware: 'guest',
data: () => ({
metaTitle: 'Register',
metaTitle: 'Register'
}),
computed: {
},
computed: {},
methods: {
}
methods: {}
}
</script>

View File

@@ -49,14 +49,17 @@
<span class="pr-1">- {{ form.submissions_count }}
submission{{ form.submissions_count > 0 ? 's' : '' }}
</span>
<span class="pr-1 text-blue-500" v-if="form.visibility=='closed'">- Closed</span>
<span class="">- Edited {{ form.last_edited_human }}</span>
<span>- Edited {{ form.last_edited_human }}</span>
</p>
<div v-if="form.visibility=='draft' || (form.tags && form.tags.length > 0)" class="mt-2 flex items-center flex-wrap gap-3">
<div v-if="['draft','closed'].includes(form.visibility) || (form.tags && form.tags.length > 0)" class="mt-2 flex items-center flex-wrap gap-3">
<span v-if="form.visibility=='draft'"
class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700">
Draft - not publicly accessible
</span>
<span v-else-if="form.visibility=='closed'"
class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700">
Closed - won't accept new submissions
</span>
<span v-for="(tag,i) in form.tags" :key="tag"
class="inline-flex items-center rounded-full bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700"
>

View File

@@ -73,12 +73,15 @@
Edited {{ form.last_edited_human }}
</li>
</ul>
<div v-if="form.visibility=='draft' || (form.tags && form.tags.length > 0)" class="mt-1 flex items-center flex-wrap gap-3">
<span v-if="form.visibility=='draft'"
class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700"
>
<div v-if="['draft','closed'].includes(form.visibility) || (form.tags && form.tags.length > 0)" class="mt-1 flex items-center flex-wrap gap-3">
<span v-if="form.visibility=='draft'"
class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700">
Draft
</span>
<span v-else-if="form.visibility=='closed'"
class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700">
Closed
</span>
<span v-for="(tag,i) in form.tags" :key="tag"
class="inline-flex items-center rounded-full bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700"
>

View File

@@ -1,14 +1,21 @@
<template>
<div>
<h3 class="font-semibold text-2xl text-gray-900">Billing details</h3>
<small class="text-gray-600">Manage your billing. Download invoices, update your plan, or cancel it at any
time.</small>
<h3 class="font-semibold text-2xl text-gray-900">
Billing details
</h3>
<div class="mt-4">
<v-button color="gray" shade="light" :loading="billingLoading" @click.prevent="openBillingDashboard">
Manage Subscription
</v-button>
</div>
<template v-if="user.has_customer_id">
<small class="text-gray-600">Manage your billing. Download invoices, update your plan, or cancel it at any
time.</small>
<div class="mt-4">
<v-button color="gray" shade="light" :loading="billingLoading" @click.prevent="openBillingDashboard">
Manage Subscription
</v-button>
</div>
</template>
<app-sumo-billing class="mt-4" />
</div>
</template>
@@ -16,11 +23,13 @@
import axios from 'axios'
import VButton from '../../components/common/Button.vue'
import SeoMeta from '../../mixins/seo-meta.js'
import { mapGetters } from 'vuex'
import AppSumoBilling from '../../components/vendor/appsumo/AppSumoBilling.vue'
export default {
components: {VButton},
scrollToTop: false,
components: { AppSumoBilling, VButton },
mixins: [SeoMeta],
scrollToTop: false,
data: () => ({
metaTitle: 'Billing',
@@ -28,7 +37,7 @@ export default {
}),
methods: {
openBillingDashboard() {
openBillingDashboard () {
this.billingLoading = true
axios.get('/api/subscription/billing-portal').then((response) => {
const url = response.data.portal_url
@@ -39,6 +48,12 @@ export default {
this.billingLoading = false
})
}
},
computed: {
...mapGetters({
user: 'auth/user'
})
}
}
</script>

View File

@@ -0,0 +1,232 @@
<template>
<div class="flex flex-col min-h-full">
<breadcrumb :path="breadcrumbs" />
<div v-if="templatesLoading" class="text-center my-4">
<loader class="h-6 w-6 text-nt-blue mx-auto" />
</div>
<p v-else-if="industry === null || !industry" class="text-center my-4">
We could not find this industry.
</p>
<template v-else>
<section class="py-12 sm:py-16 bg-gray-50 border-b border-gray-200">
<div class="px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto">
<div class="text-center mx-auto">
<div class="font-semibold sm:w-full text-blue-500 mb-3">
{{ industry.name }}
</div>
<h1 class="text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight text-gray-900">
{{ industry.meta_title }}
</h1>
<p class="max-w-xl mx-auto text-gray-600 mt-4 text-lg font-normal">
{{ industry.meta_description }}
</p>
</div>
</div>
</section>
<section class="bg-white py-12 sm:py-16">
<div class="px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 sm:gap-6 relative z-20">
<div class="flex items-center gap-4">
<div class="flex-1 sm:flex-none">
<select-input v-model="selectedType" name="type"
:options="typesOptions" class="w-full sm:w-auto md:w-56"
/>
</div>
</div>
<div class="flex-1 w-full md:max-w-xs">
<text-input name="search" :form="searchTemplate" placeholder="Search..." />
</div>
</div>
<div v-if="templatesLoading" class="text-center mt-4">
<loader class="h-6 w-6 text-nt-blue mx-auto" />
</div>
<p v-else-if="enrichedTemplates.length === 0" class="text-center mt-4">
No templates found.
</p>
<div v-else class="relative z-10">
<div class="grid grid-cols-1 mt-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8 sm:gap-y-12">
<single-template v-for="template in enrichedTemplates" :key="template.id" :slug="template.slug" />
</div>
</div>
</div>
</section>
<section class="py-12 bg-white border-t border-gray-200 sm:py-16">
<div class="px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl">
<p class="text-gray-600 font-normal">
{{ industry.description }}
</p>
</div>
</section>
<section class="py-12 bg-white border-t border-gray-200 sm:py-16">
<div class="px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl">
<div class="flex items-center justify-between">
<h4 class="text-xl font-bold tracking-tight text-gray-900 sm:text-2xl">
Other Industries
</h4>
<v-button :to="{name:'templates'}" color="white" size="small" :arrow="true">
View All Templates
</v-button>
</div>
<div class="grid grid-cols-1 gap-8 mt-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<router-link v-for="row in otherIndustries" :key="row.slug"
:to="{params:{slug:row.slug}, name:'templates.industries.show'}"
:title="row.name"
class="text-gray-600 dark:text-gray-400 transition-colors duration-300 hover:text-nt-blue"
>
{{ row.name }}
</router-link>
</div>
</div>
</section>
</template>
<open-form-footer class="mt-8 border-t"/>
</div>
</template>
<script>
import store from '~/store'
import Form from 'vform'
import Fuse from 'fuse.js'
import { mapGetters, mapState } from 'vuex'
import SeoMeta from '../../mixins/seo-meta.js'
import OpenFormFooter from '../../components/pages/OpenFormFooter.vue'
import Breadcrumb from '../../components/common/Breadcrumb.vue'
import SingleTemplate from '../../components/pages/templates/SingleTemplate.vue'
const loadTemplates = function () {
store.commit('open/templates/startLoading')
store.dispatch('open/templates/loadIfEmpty').then(() => {
store.commit('open/templates/stopLoading')
})
}
export default {
components: { Breadcrumb, OpenFormFooter, SingleTemplate },
mixins: [SeoMeta],
beforeRouteEnter (to, from, next) {
loadTemplates()
next()
},
data () {
return {
selectedType: 'all',
searchTemplate: new Form({
search: ''
})
}
},
mounted () {},
computed: {
...mapGetters({
authenticated: 'auth/check',
user: 'auth/user'
}),
...mapState({
templates: state => state['open/templates'].content,
templatesLoading: state => state['open/templates'].loading,
industries: state => state['open/templates'].industries,
types: state => state['open/templates'].types
}),
breadcrumbs () {
if (!this.industry) {
return [{ route: { name: 'templates' }, label: 'Templates' }]
}
return [{ route: { name: 'templates' }, label: 'Templates' }, { label: this.industry.name }]
},
industry () {
return Object.values(this.industries).find((industry) => {
return industry.slug === this.$route.params.slug
})
},
typesOptions () {
return [{ name: 'All Types', value: 'all' }].concat(Object.values(this.types).map((type) => {
return {
name: type.name,
value: type.slug
}
}))
},
otherIndustries() {
return Object.values(this.industries).filter((industry) => {
return industry.slug !== this.$route.params.slug
})
},
enrichedTemplates () {
let enrichedTemplates = this.templates
// Filter by current Industry only
enrichedTemplates = enrichedTemplates.filter((item) => {
return (item.industries && item.industries.length > 0) ? item.industries.includes(this.$route.params.slug) : false
})
// Filter by Selected Type
if (this.selectedType && this.selectedType !== 'all') {
enrichedTemplates = enrichedTemplates.filter((item) => {
return (item.types && item.types.length > 0) ? item.types.includes(this.selectedType) : false
})
}
if (this.searchTemplate.search === '' || this.searchTemplate.search === null) {
return enrichedTemplates
}
// Fuze search
const fuzeOptions = {
keys: [
'name',
'slug',
'description',
'short_description'
]
}
const fuse = new Fuse(enrichedTemplates, fuzeOptions)
return fuse.search(this.searchTemplate.search).map((res) => {
return res.item
})
},
metaTitle () {
return this.industry ? this.industry.meta_title : 'Form Template Industry'
},
metaDescription () {
if (!this.industry) return null
return this.industry.meta_description.substring(0, 140)
}
},
methods: {}
}
</script>
<style lang='scss'>
.nf-text {
@apply space-y-4;
h2 {
@apply text-sm font-normal tracking-widest text-gray-500 uppercase;
}
p {
@apply font-normal leading-7 text-gray-900 dark:text-gray-100;
}
ol {
@apply list-decimal list-inside;
}
ul {
@apply list-disc list-inside;
}
}
</style>

View File

@@ -0,0 +1,233 @@
<template>
<div class="flex flex-col min-h-full">
<breadcrumb :path="breadcrumbs" />
<div v-if="templatesLoading" class="text-center my-4">
<loader class="h-6 w-6 text-nt-blue mx-auto" />
</div>
<p v-else-if="type === null || !type" class="text-center my-4">
We could not find this type.
</p>
<template v-else>
<section class="py-12 sm:py-16 bg-gray-50 border-b border-gray-200">
<div class="px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto">
<div class="text-center mx-auto">
<div class="font-semibold sm:w-full text-blue-500 mb-3">
{{ type.name }}
</div>
<h1 class="text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight text-gray-900">
{{ type.meta_title }}
</h1>
<p class="max-w-xl mx-auto text-gray-600 mt-4 text-lg font-normal">
{{ type.meta_description }}
</p>
</div>
</div>
</section>
<section class="bg-white py-12 sm:py-16">
<div class="px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 sm:gap-6 relative z-20">
<div class="flex items-center gap-4">
<div class="flex-1 sm:flex-none">
<select-input v-model="selectedIndustry" name="industry"
:options="industriesOptions" class="w-full sm:w-auto md:w-56"
/>
</div>
</div>
<div class="flex-1 w-full md:max-w-xs">
<text-input name="search" :form="searchTemplate" placeholder="Search..." />
</div>
</div>
<div v-if="templatesLoading" class="text-center mt-4">
<loader class="h-6 w-6 text-nt-blue mx-auto" />
</div>
<p v-else-if="enrichedTemplates.length === 0" class="text-center mt-4">
No templates found.
</p>
<div v-else class="relative z-10">
<div class="grid grid-cols-1 mt-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8 sm:gap-y-12">
<single-template v-for="template in enrichedTemplates" :key="template.id" :slug="template.slug" />
</div>
</div>
</div>
</section>
<section class="py-12 bg-white border-t border-gray-200 sm:py-16">
<div class="px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl">
<p class="text-gray-600 font-normal">
{{ type.description }}
</p>
</div>
</section>
<section class="py-12 bg-white border-t border-gray-200 sm:py-16">
<div class="px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl">
<div class="flex items-center justify-between">
<h4 class="text-xl font-bold tracking-tight text-gray-900 sm:text-2xl">
Other Types
</h4>
<v-button :to="{name:'templates'}" color="white" size="small" :arrow="true">
View All Templates
</v-button>
</div>
<div class="grid grid-cols-1 gap-8 mt-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<router-link v-for="row in otherTypes" :key="row.slug"
:to="{params:{slug:row.slug}, name:'templates.types.show'}"
:title="row.name"
class="text-gray-600 dark:text-gray-400 transition-colors duration-300 hover:text-nt-blue"
>
{{ row.name }}
</router-link>
</div>
</div>
</section>
</template>
<open-form-footer class="mt-8 border-t"/>
</div>
</template>
<script>
import store from '~/store'
import Form from 'vform'
import Fuse from 'fuse.js'
import { mapGetters, mapState } from 'vuex'
import SeoMeta from '../../mixins/seo-meta.js'
import OpenFormFooter from '../../components/pages/OpenFormFooter.vue'
import Breadcrumb from '../../components/common/Breadcrumb.vue'
import SingleTemplate from '../../components/pages/templates/SingleTemplate.vue'
const loadTemplates = function () {
store.commit('open/templates/startLoading')
store.dispatch('open/templates/loadIfEmpty').then(() => {
store.commit('open/templates/stopLoading')
})
}
export default {
components: { Breadcrumb, OpenFormFooter, SingleTemplate },
mixins: [SeoMeta],
beforeRouteEnter (to, from, next) {
loadTemplates()
next()
},
data () {
return {
selectedIndustry: 'all',
searchTemplate: new Form({
search: ''
})
}
},
mounted () {},
computed: {
...mapGetters({
authenticated: 'auth/check',
user: 'auth/user'
}),
...mapState({
templates: state => state['open/templates'].content,
templatesLoading: state => state['open/templates'].loading,
industries: state => state['open/templates'].industries,
types: state => state['open/templates'].types
}),
breadcrumbs () {
if (!this.type) {
return [{ route: { name: 'templates' }, label: 'Templates' }]
}
return [{ route: { name: 'templates' }, label: 'Templates' }, { label: this.type.name }]
},
type () {
return Object.values(this.types).find((type) => {
return type.slug === this.$route.params.slug
})
},
industriesOptions () {
return [{ name: 'All Industries', value: 'all' }].concat(Object.values(this.industries).map((industry) => {
return {
name: industry.name,
value: industry.slug
}
}))
},
otherTypes() {
return Object.values(this.types).filter((type) => {
return type.slug !== this.$route.params.slug
})
},
enrichedTemplates () {
let enrichedTemplates = this.templates
// Filter by current Type only
enrichedTemplates = enrichedTemplates.filter((item) => {
return (item.types && item.types.length > 0) ? item.types.includes(this.$route.params.slug) : false
})
// Filter by Selected Industry
if (this.selectedIndustry && this.selectedIndustry !== 'all') {
enrichedTemplates = enrichedTemplates.filter((item) => {
return (item.industries && item.industries.length > 0) ? item.industries.includes(this.selectedIndustry) : false
})
}
if (this.searchTemplate.search === '' || this.searchTemplate.search === null) {
return enrichedTemplates
}
// Fuze search
const fuzeOptions = {
keys: [
'name',
'slug',
'description',
'short_description'
]
}
const fuse = new Fuse(enrichedTemplates, fuzeOptions)
return fuse.search(this.searchTemplate.search).map((res) => {
return res.item
})
},
metaTitle () {
return this.type ? this.type.meta_title : 'Form Template Type'
},
metaDescription () {
if (!this.type) return null
return this.type.meta_description.substring(0, 140)
}
},
methods: {}
}
</script>
<style lang='scss'>
.nf-text {
@apply space-y-4;
h2 {
@apply text-sm font-normal tracking-widest text-gray-500 uppercase;
}
p {
@apply font-normal leading-7 text-gray-900 dark:text-gray-100;
}
ol {
@apply list-decimal list-inside;
}
ul {
@apply list-disc list-inside;
}
}
</style>