Work in progress

This commit is contained in:
Julien Nahum
2023-12-09 15:47:03 +01:00
parent f970557b76
commit 1f853e8178
315 changed files with 34058 additions and 25 deletions

View File

@@ -0,0 +1,548 @@
<template>
<div class="flex-1">
<!-- START HERO -->
<section class="bg-gradient-to-b relative from-white to-gray-100 py-12 sm:py-16 lg:py-20 xl:py-24">
<div class="absolute inset-0">
<img class="w-full h-full object-cover object-top" src="/img/pages/ai_form_builder/background-pattern.svg" alt="">
</div>
<div class="px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto relative">
<div class="max-w-4xl mx-auto text-center">
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-semibold text-gray-900 tracking-tight">
Say goodbye to tedious form building with OpnForm's new <span
class="bg-clip-text text-transparent bg-gradient-to-r lg:block from-blue-600 to-blue-400">AI-powered
feature!</span>
</h1>
<p class="mt-4 sm:mt-5 text-base leading-7 sm:text-xl sm:leading-9 font-medium text-gray-500">
Easily generate a fully working form in seconds with just a simple description.
</p>
<div class="mt-8 flex justify-center">
<v-button v-if="!authenticated" class="mr-1" :to="{ name: 'forms.create.guest' }" :arrow="true">
Get started for free
</v-button>
<v-button v-else class="mr-1" :to="{ name: 'forms.create' }" :arrow="true">
Get started for free
</v-button>
</div>
</div>
<div class="max-w-6xl mx-auto mt-12 sm:mt-16">
<div
class="-m-2 rounded-xl bg-blue-900/5 p-2 backdrop-blur-sm ring-1 ring-inset ring-blue-900/10 lg:-m-4 lg:rounded-2xl lg:p-4">
<video class="rounded-md ring-1 ring-gray-200 shadow-xl shadow-blue-600/10 ring-blue-900/10" controls autoplay loop muted>
<source src="/video/opnform-ai.mp4" type="video/mp4">
This browser does not display the video tag.
</video>
</div>
</div>
<div
class="grid lg:grid-cols-3 grid-cols-1 sm:max-w-lg lg:max-w-5xl sm:mx-auto gap-8 sm:gap-10 mt-12 sm:mt-16">
<div class="flex items-start gap-4">
<img class="w-12 h-12 shrink-0" src="/img/pages/ai_form_builder/icon-fast.svg" alt="">
<div>
<p class="text-md font-semibold text-gray-900">
Faster than Ever
</p>
<p class="text-base font-medium text-gray-500 mt-2">
Save time and effort by generating a form in seconds
</p>
</div>
</div>
<div class="flex items-start gap-4">
<img class="w-12 h-12 shrink-0" src="/img/pages/ai_form_builder/icon-customization.svg" alt="">
<div>
<p class="text-md font-semibold text-gray-900">
Customizations
</p>
<p class="text-base font-medium text-gray-500 mt-2">
Customize your form to your exact specifications
</p>
</div>
</div>
<div class="flex items-start gap-4">
<img class="w-12 h-12 shrink-0" src="/img/pages/ai_form_builder/icon-browser.svg" alt="">
<div>
<p class="text-md font-semibold text-gray-900">
No Coding Knowledge Required
</p>
<p class="text-base font-medium text-gray-500 mt-2">
No coding knowledge required and it's completely free to use
</p>
</div>
</div>
</div>
</div>
</section>
<!-- END HERO -->
<!-- START HOW IT WORKS -->
<section class="bg-white py-12 sm:py-16 lg:py-20 xl:py-24">
<div class="px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto">
<div class="text-center max-w-3xl mx-auto">
<h2 class="text-sm font-semibold text-blue-600">
How Does It Work?
</h2>
<p
class="text-3xl mt-4 sm:text-4xl lg:text-5xl font-semibold text-gray-900 tracking-tight lg:leading-tight">
Save hours in just a few clicks
</p>
<p class="text-gray-500 text-base leading-7 sm:text-lg sm:leading-8 font-medium mt-4">
Building forms has never been easier
</p>
</div>
<div
class="space-y-16 text-center lg:text-left sm:max-w-md sm:mx-auto lg:max-w-none lg:space-y-20 xl:space-y-24 mt-8 sm:mt-12 lg:mt-16">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-8 lg:gap-12">
<img class="w-full lg:flex-1 bg-gray-300 lg:shrink-0 rounded-2xl ring-1 ring-gray-200 shadow-sm"
src="/img/pages/ai_form_builder/step-1.svg" alt="">
<div
class="w-16 h-16 rounded-full bg-blue-50 border-2 border-blue-200 hidden xl:inline-flex items-center justify-center text-blue-600 text-2xl font-semibold leading-none">
1
</div>
<div class="lg:flex-1 lg:shrink-0">
<img class="w-auto h-16 hidden lg:block" src="/img/pages/ai_form_builder/icon-create.svg" alt="">
<h3 class="text-2xl sm:text-3xl lg:text-4xl font-semibold text-gray-900 lg:mt-8">
Building forms made easy
</h3>
<p class="text-base font-medium leading-7 sm:text-lg sm:leading-8 text-gray-500 mt-4">
OpnForm's easy-to-use online form creator lets you create a beautiful web form in no time. Whether you
need to create
contact forms and registration forms for a landing page or an online order form for your business, you
will no longer
need to spend hours working on forms.
</p>
</div>
</div>
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-8 lg:gap-12">
<img
class="w-full lg:flex-1 bg-gray-300 lg:shrink-0 rounded-2xl ring-1 ring-gray-200 shadow-sm lg:order-3"
src="/img/pages/ai_form_builder/step-2.svg" alt="">
<div
class="w-16 h-16 lg:order-2 rounded-full bg-blue-50 border-2 border-blue-200 hidden xl:inline-flex items-center justify-center text-blue-600 text-2xl font-semibold leading-none">
2
</div>
<div class="lg:flex-1 lg:shrink-0 lg:order-1">
<img class="w-auto h-16 hidden lg:block" src="/img/pages/ai_form_builder/icon-customization.svg" alt="">
<h3 class="text-2xl sm:text-3xl lg:text-4xl font-semibold text-gray-900 lg:mt-8">
Customized forms work wonders
</h3>
<p class="text-base font-medium leading-7 sm:text-lg sm:leading-8 text-gray-500 mt-4">
Did you know that good-looking forms are more likely to convert more responses than ordinary ones? On
OpnForm, you can
customize your form design to the tiniest detail and match your brand image with your forms.
</p>
</div>
</div>
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-8 lg:gap-12">
<img class="w-full lg:flex-1 bg-gray-300 lg:shrink-0 rounded-2xl ring-1 ring-gray-200 shadow-sm"
src="/img/pages/ai_form_builder/step-3.svg" alt="">
<div
class="w-16 h-16 rounded-full bg-blue-50 border-2 border-blue-200 hidden xl:inline-flex items-center justify-center text-blue-600 text-2xl font-semibold leading-none">
3
</div>
<div class="lg:flex-1 lg:shrink-0">
<img class="w-auto h-16 hidden lg:block" src="/img/pages/ai_form_builder/icon-share.svg" alt="">
<h3 class="text-2xl sm:text-3xl lg:text-4xl font-semibold text-gray-900 lg:mt-8">
Share your forms anywhere
</h3>
<p class="text-base font-medium leading-7 sm:text-lg sm:leading-8 text-gray-500 mt-4">
You can share your forms anywhere using their unique weblink or embed them on your landing pages
seamlessly.
Furthermore, you can adjust your forms privacy settings in order to reach the targeted audience and
prevent others from
viewing your form.
</p>
</div>
</div>
</div>
</div>
</section>
<!-- END HOW IT WORKS -->
<!-- START EXAMPLES -->
<!-- <section class="bg-gray-50 py-12 sm:py-16 lg:py-20 xl:py-24">-->
<!-- <div class="px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto">-->
<!-- <div class="text-center max-w-3xl mx-auto">-->
<!-- <h2 class="text-sm font-semibold text-blue-600">-->
<!-- Examples-->
<!-- </h2>-->
<!-- <p-->
<!-- class="text-3xl mt-4 sm:text-4xl lg:text-5xl font-semibold text-gray-900 tracking-tight lg:leading-tight">-->
<!-- What our user has created-->
<!-- </p>-->
<!-- <p class="text-gray-500 text-base leading-7 sm:text-lg sm:leading-8 font-medium mt-4">-->
<!-- Amet minim mollit non deserunt ullamco est sit aliqua dolor do amet sint. Velit officia consequat duis-->
<!-- enim velit-->
<!-- mollit.-->
<!-- </p>-->
<!-- </div>-->
<!-- <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mt-8 sm:mt-12 lg:mt-16">-->
<!-- <div-->
<!-- class="bg-white overflow-hidden rounded-2xl ring-1 ring-gray-200 shadow-sm hover:shadow-lg hover:-translate-y-2 transition-all duration-150">-->
<!-- <img class="w-full" src="/img/pages/ai_form_builder/examples-placeholder.png" alt="">
-->
<!-- <div class="px-4 py-5 sm:p-6">-->
<!-- <h3 class="text-lg font-semibold text-gray-900">-->
<!-- Example 1-->
<!-- </h3>-->
<!-- <p class="text-base font-medium text-gray-500 mt-2 line-clamp-2">-->
<!-- Nulla Lorem mollit cupidatat irure. Laborum magna nulla duis ullamco cillum dolor-->
<!-- </p>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div-->
<!-- class="bg-white overflow-hidden rounded-2xl ring-1 ring-gray-200 shadow-sm hover:shadow-lg hover:-translate-y-2 transition-all duration-150">-->
<!-- <img class="w-full" src="/img/pages/ai_form_builder/examples-placeholder.png" alt="">
-->
<!-- <div class="px-4 py-5 sm:p-6">-->
<!-- <h3 class="text-lg font-semibold text-gray-900">-->
<!-- Example 2-->
<!-- </h3>-->
<!-- <p class="text-base font-medium text-gray-500 mt-2 line-clamp-2">-->
<!-- Nulla Lorem mollit cupidatat irure. Laborum magna nulla duis ullamco cillum dolor-->
<!-- </p>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div-->
<!-- class="bg-white overflow-hidden rounded-2xl ring-1 ring-gray-200 shadow-sm hover:shadow-lg hover:-translate-y-2 transition-all duration-150">-->
<!-- <img class="w-full" src="/img/pages/ai_form_builder/examples-placeholder.png" alt="">
-->
<!-- <div class="px-4 py-5 sm:p-6">-->
<!-- <h3 class="text-lg font-semibold text-gray-900">-->
<!-- Example 3-->
<!-- </h3>-->
<!-- <p class="text-base font-medium text-gray-500 mt-2 line-clamp-2">-->
<!-- Nulla Lorem mollit cupidatat irure. Laborum magna nulla duis ullamco cillum dolor-->
<!-- </p>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div-->
<!-- class="bg-white overflow-hidden rounded-2xl ring-1 ring-gray-200 shadow-sm hover:shadow-lg hover:-translate-y-2 transition-all duration-150">-->
<!-- <img class="w-full" src="/img/pages/ai_form_builder/examples-placeholder.png" alt="">
-->
<!-- <div class="px-4 py-5 sm:p-6">-->
<!-- <h3 class="text-lg font-semibold text-gray-900">-->
<!-- Example 4-->
<!-- </h3>-->
<!-- <p class="text-base font-medium text-gray-500 mt-2 line-clamp-2">-->
<!-- Nulla Lorem mollit cupidatat irure. Laborum magna nulla duis ullamco cillum dolor-->
<!-- </p>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!--&lt;!&ndash; <hr class="mt-12 border-gray-200 sm:mt-16">&ndash;&gt;-->
<!--&lt;!&ndash; <div class="max-w-2xl mx-auto mt-12 text-center sm:mt-16">&ndash;&gt;-->
<!--&lt;!&ndash; <h4 class="text-2xl font-semibold tracking-tight text-gray-900 sm:text-3xl lg:text-4xl">&ndash;&gt;-->
<!--&lt;!&ndash; Ready to level-up?&ndash;&gt;-->
<!--&lt;!&ndash; </h4>&ndash;&gt;-->
<!--&lt;!&ndash; <p class="mt-4 text-base leading-7 sm:text-xl sm:leading-9 font-medium text-gray-500">&ndash;&gt;-->
<!--&lt;!&ndash; Save time and effortlessly create forms with OpnForm&ndash;&gt;-->
<!--&lt;!&ndash; </p>&ndash;&gt;-->
<!--&lt;!&ndash; <div class="mt-8 flex justify-center">&ndash;&gt;-->
<!--&lt;!&ndash; <v-button v-if="!authenticated" class="mr-1" :to="{ name: 'forms.create.guest' }" :arrow="true">&ndash;&gt;-->
<!--&lt;!&ndash; Get started for free&ndash;&gt;-->
<!--&lt;!&ndash; </v-button>&ndash;&gt;-->
<!--&lt;!&ndash; <v-button v-else class="mr-1" :to="{ name: 'forms.create' }" :arrow="true">&ndash;&gt;-->
<!--&lt;!&ndash; Get started for free&ndash;&gt;-->
<!--&lt;!&ndash; </v-button>&ndash;&gt;-->
<!--&lt;!&ndash; </div>&ndash;&gt;-->
<!--&lt;!&ndash; <ul&ndash;&gt;-->
<!--&lt;!&ndash; class="flex mt-8 sm:mt-12 text-sm font-medium text-gray-900 items-center justify-center flex-wrap gap-x-6 gap-y-4">&ndash;&gt;-->
<!--&lt;!&ndash; <li class="flex items-center gap-2">&ndash;&gt;-->
<!--&lt;!&ndash; <svg aria-hidden="true" class="h-5 w-5 text-gray-400 shrink-0" xmlns="http://www.w3.org/2000/svg"&ndash;&gt;-->
<!--&lt;!&ndash; viewBox="0 0 20 20" fill="currentColor">&ndash;&gt;-->
<!--&lt;!&ndash; <path fill-rule="evenodd"&ndash;&gt;-->
<!--&lt;!&ndash; d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"&ndash;&gt;-->
<!--&lt;!&ndash; clip-rule="evenodd" />&ndash;&gt;-->
<!--&lt;!&ndash; </svg>&ndash;&gt;-->
<!--&lt;!&ndash; No design skills required&ndash;&gt;-->
<!--&lt;!&ndash; </li>&ndash;&gt;-->
<!--&lt;!&ndash; <li class="flex items-center gap-2">&ndash;&gt;-->
<!--&lt;!&ndash; <svg aria-hidden="true" class="h-5 w-5 text-gray-400 shrink-0" xmlns="http://www.w3.org/2000/svg"&ndash;&gt;-->
<!--&lt;!&ndash; viewBox="0 0 20 20" fill="currentColor">&ndash;&gt;-->
<!--&lt;!&ndash; <path fill-rule="evenodd"&ndash;&gt;-->
<!--&lt;!&ndash; d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"&ndash;&gt;-->
<!--&lt;!&ndash; clip-rule="evenodd" />&ndash;&gt;-->
<!--&lt;!&ndash; </svg>&ndash;&gt;-->
<!--&lt;!&ndash; Setup in minutes&ndash;&gt;-->
<!--&lt;!&ndash; </li>&ndash;&gt;-->
<!--&lt;!&ndash; <li class="flex items-center gap-2">&ndash;&gt;-->
<!--&lt;!&ndash; <svg aria-hidden="true" class="h-5 w-5 text-gray-400 shrink-0" xmlns="http://www.w3.org/2000/svg"&ndash;&gt;-->
<!--&lt;!&ndash; viewBox="0 0 20 20" fill="currentColor">&ndash;&gt;-->
<!--&lt;!&ndash; <path fill-rule="evenodd"&ndash;&gt;-->
<!--&lt;!&ndash; d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"&ndash;&gt;-->
<!--&lt;!&ndash; clip-rule="evenodd" />&ndash;&gt;-->
<!--&lt;!&ndash; </svg>&ndash;&gt;-->
<!--&lt;!&ndash; It's free&ndash;&gt;-->
<!--&lt;!&ndash; </li>&ndash;&gt;-->
<!--&lt;!&ndash; </ul>&ndash;&gt;-->
<!--&lt;!&ndash; </div>&ndash;&gt;-->
<!-- </div>-->
<!-- </section>-->
<!-- END EXAMPLES -->
<!-- START TESTIMONIALS -->
<!-- <section class="bg-white py-12 sm:py-16 lg:py-20 xl:py-24">-->
<!-- <div class="px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto">-->
<!-- <div class="text-center max-w-3xl mx-auto">-->
<!-- <h2 class="text-sm font-semibold text-blue-600">-->
<!-- Customer Testimonials-->
<!-- </h2>-->
<!-- <p-->
<!-- class="text-3xl mt-4 sm:text-4xl lg:text-5xl font-semibold text-gray-900 tracking-tight lg:leading-tight">-->
<!-- See what people are saying-->
<!-- </p>-->
<!-- <p class="text-gray-500 text-base leading-7 sm:text-lg sm:leading-8 font-medium mt-4">-->
<!-- These are the stories of our customers who have joined us with great pleasure when using this crazy-->
<!-- feature.-->
<!-- </p>-->
<!-- </div>-->
<!-- </div>-->
<!-- </section>-->
<!-- END TESTIMONIALS -->
<!-- START FAQS -->
<!-- <section class="bg-white py-12 sm:py-16 lg:py-20 xl:py-24 border-t border-gray-200">-->
<!-- <div class="px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto">-->
<!-- <div class="text-center max-w-3xl mx-auto">-->
<!-- <h2 class="text-3xl sm:text-4xl lg:text-5xl font-semibold text-gray-900 tracking-tight lg:leading-tight">-->
<!-- Frequently Asked Questions-->
<!-- </h2>-->
<!-- <p class="text-gray-500 text-base leading-7 sm:text-lg sm:leading-8 font-medium mt-4">-->
<!-- We've compiled a list of the most common questions we get asked.-->
<!-- </p>-->
<!-- </div>-->
<!-- <div class="mt-12 sm:mt-16 lg:mt-20">-->
<!-- <dl class="gap-y-12 grid grid-cols-1 sm:grid-cols-2 sm:gap-x-8 sm:gap-y-16 lg:gap-x-10">-->
<!-- <div>-->
<!-- <dt class="sm:text-lg text-base leading-7 font-medium sm:leading-8 text-gray-900">-->
<!-- What's the best thing about Switzerland?-->
<!-- </dt>-->
<!-- <dd class="mt-2 text-base font-medium leading-7 text-gray-500">-->
<!-- I don't know, but the flag is a big plus. Lorem ipsum dolor sit amet consectetur adipisicing elit.-->
<!-- Quas cupiditate-->
<!-- laboriosam fugiat.-->
<!-- </dd>-->
<!-- </div>-->
<!-- <div>-->
<!-- <dt class="sm:text-lg text-base leading-7 font-medium sm:leading-8 text-gray-900">-->
<!-- How do you make holy water?-->
<!-- </dt>-->
<!-- <dd class="mt-2 text-base font-medium leading-7 text-gray-500">-->
<!-- You boil the hell out of it. Lorem ipsum dolor sit amet consectetur adipisicing elit. Magnam aut-->
<!-- tempora vitae odio-->
<!-- inventore fuga aliquam nostrum quod porro. Delectus quia facere id sequi expedita natus.-->
<!-- </dd>-->
<!-- </div>-->
<!-- <div>-->
<!-- <dt class="sm:text-lg text-base leading-7 font-medium sm:leading-8 text-gray-900">-->
<!-- What do you call someone with no body and no nose?-->
<!-- </dt>-->
<!-- <dd class="mt-2 text-base font-medium leading-7 text-gray-500">-->
<!-- Nobody knows. Lorem ipsum dolor sit amet consectetur adipisicing elit. Culpa, voluptas ipsa quia-->
<!-- excepturi, quibusdam-->
<!-- natus exercitationem sapiente tempore labore voluptatem.-->
<!-- </dd>-->
<!-- </div>-->
<!-- <div>-->
<!-- <dt class="sm:text-lg text-base leading-7 font-medium sm:leading-8 text-gray-900">-->
<!-- Why do you never see elephants hiding in trees?-->
<!-- </dt>-->
<!-- <dd class="mt-2 text-base font-medium leading-7 text-gray-500">-->
<!-- Because they're so good at it. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quas-->
<!-- cupiditate laboriosam-->
<!-- fugiat.-->
<!-- </dd>-->
<!-- </div>-->
<!-- <div>-->
<!-- <dt class="sm:text-lg text-base leading-7 font-medium sm:leading-8 text-gray-900">-->
<!-- Why can't you hear a pterodactyl go to the bathroom?-->
<!-- </dt>-->
<!-- <dd class="mt-2 text-base font-medium leading-7 text-gray-500">-->
<!-- Because the pee is silent. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ipsam, quas-->
<!-- voluptatibus ex culpa-->
<!-- ipsum, aspernatur blanditiis fugiat ullam magnam suscipit deserunt illum natus facilis atque vero-->
<!-- consequatur! Quisquam,-->
<!-- debitis error.-->
<!-- </dd>-->
<!-- </div>-->
<!-- <div>-->
<!-- <dt class="sm:text-lg text-base leading-7 font-medium sm:leading-8 text-gray-900">-->
<!-- Why did the invisible man turn down the job offer?-->
<!-- </dt>-->
<!-- <dd class="mt-2 text-base font-medium leading-7 text-gray-500">-->
<!-- He couldn't see himself doing it. Lorem ipsum dolor sit, amet consectetur adipisicing elit. Eveniet-->
<!-- perspiciatis-->
<!-- officiis corrupti tenetur. Temporibus ut voluptatibus, perferendis sed unde rerum deserunt eius.-->
<!-- </dd>-->
<!-- </div>-->
<!-- </dl>-->
<!-- </div>-->
<!-- </div>-->
<!-- </section>-->
<!-- END FAQS -->
<!-- START CTA -->
<section class="bg-gradient-to-b from-gray-100 to-white py-12 sm:py-16 lg:pt-20 xl:pt-24">
<div class="px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto">
<div class="flex items-center justify-center gap-8">
<img class="w-auto h-12" src="/img/pages/ai_form_builder/icon-email-input.svg" alt="">
<img class="w-auto h-12" src="/img/pages/ai_form_builder/icon-radio-buttons.svg" alt="">
<img class="w-auto h-12" src="/img/pages/ai_form_builder/icon-textarea.svg" alt="">
</div>
<div class="text-center max-w-3xl mx-auto mt-8 sm:mt-12">
<h2 class="text-3xl sm:text-4xl lg:text-5xl font-semibold text-gray-900 tracking-tight lg:leading-tight">
Create beautiful forms and share them anywhere
</h2>
<p class="mt-4 sm:mt-5 text-base leading-7 sm:text-xl sm:leading-9 font-medium text-gray-500">
It takes seconds, you don't need to know how to code and <span class="text-blue-600">it's free</span>.
</p>
<div class="mt-8 flex justify-center">
<v-button v-if="!authenticated" class="mr-1" :to="{ name: 'forms.create.guest' }" :arrow="true">
Get started for free
</v-button>
<v-button v-else class="mr-1" :to="{ name: 'forms.create' }" :arrow="true">
Get started for free
</v-button>
</div>
<ul
class="flex mt-8 sm:mt-12 text-sm font-medium text-gray-900 items-center justify-center flex-wrap gap-x-6 gap-y-4">
<li class="flex items-center gap-2">
<svg aria-hidden="true" class="h-5 w-5 text-gray-400 shrink-0" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd" />
</svg>
No design skills required
</li>
<li class="flex items-center gap-2">
<svg aria-hidden="true" class="h-5 w-5 text-gray-400 shrink-0" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd" />
</svg>
Setup in minutes
</li>
<li class="flex items-center gap-2">
<svg aria-hidden="true" class="h-5 w-5 text-gray-400 shrink-0" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd" />
</svg>
Free plan available
</li>
</ul>
</div>
</div>
</section>
<!-- END CTA -->
<open-form-footer class="dark:border-t border-t"/>
</div>
</template>
<script>
import { computed } from 'vue'
import { useAuthStore } from '../stores/auth'
import OpenFormFooter from '../components/pages/OpenFormFooter.vue'
import SeoMeta from '../mixins/seo-meta.js'
export default {
components: {OpenFormFooter},
layout: 'default',
mixins: [SeoMeta],
setup () {
const authStore = useAuthStore()
return {
authenticated : computed(() => authStore.check),
}
},
data: () => ({
title: 'OpnForm',
metaTitle: 'AI form builder for free',
}),
mounted() {},
methods: {},
computed: {
configLinks: () => this.$config.links
}
}
</script>
<style lang="scss" scoped>
.customer-logo-container {
max-width: 130px;
width: 100%;
}
.ticks {
color:#2563eb;
}
@screen md {
#macbook-video {
position: absolute;
max-width: 84.8% !important;
right: 0px;
top: 6.8%;
}
}
</style>

View File

@@ -0,0 +1,56 @@
<template>
<div>
<div class="flex mt-6 mb-10">
<div class="w-full md:w-2/3 md:mx-auto md:max-w-md px-4">
<h1 class="my-6">
Reset password
</h1>
<form @submit.prevent="send" @keydown="form.onKeydown($event)">
<alert-success :form="form" :message="status" class="mb-4" />
<!-- Email -->
<text-input name="email" :form="form" label="Email" :required="true" />
<!-- Submit Button -->
<v-button class="w-full" :loading="form.busy">
Send Password Reset Link
</v-button>
</form>
</div>
</div>
<open-form-footer />
</div>
</template>
<script>
import Form from 'vform'
import OpenFormFooter from '../../../components/pages/OpenFormFooter.vue'
import SeoMeta from '../../../mixins/seo-meta.js'
export default {
components: {
OpenFormFooter
},
mixins: [SeoMeta],
middleware: 'guest',
data: () => ({
metaTitle: 'Reset Password',
status: '',
form: new Form({
email: ''
})
}),
methods: {
async send () {
const { data } = await this.form.post('/api/password/email')
this.status = data.status
this.form.reset()
}
}
}
</script>

View File

@@ -0,0 +1,74 @@
<template>
<div>
<div class="flex mt-6 mb-10">
<div class="w-full md:w-2/3 md:mx-auto md:max-w-md px-4">
<h1 class="my-6">
Reset Password
</h1>
<form @submit.prevent="reset" @keydown="form.onKeydown($event)">
<alert-success class="mb-4" :form="form" :message="status" />
<!-- Email -->
<text-input name="email" :form="form" label="Email" :required="true" />
<!-- Password -->
<text-input class="mt-8" native-type="password"
name="password" :form="form" label="Password" :required="true"
/>
<!-- Password Confirmation-->
<text-input class="mt-8" native-type="password"
name="password_confirmation" :form="form" label="Confirm Password" :required="true"
/>
<!-- Submit Button -->
<v-button class="w-full" :loading="form.busy">
Reset Password
</v-button>
</form>
</div>
</div>
<open-form-footer />
</div>
</template>
<script>
import Form from 'vform'
import OpenFormFooter from '../../../components/pages/OpenFormFooter.vue'
import SeoMeta from '../../../mixins/seo-meta.js'
export default {
components: {
OpenFormFooter
},
mixins: [SeoMeta],
middleware: 'guest',
data: () => ({
metaTitle: 'Reset Password',
status: '',
form: new Form({
token: '',
email: '',
password: '',
password_confirmation: ''
})
}),
created () {
this.form.email = this.$route.query.email
this.form.token = this.$route.params.token
},
methods: {
async reset () {
const { data } = await this.form.post('/api/password/reset')
this.status = data.status
this.form.reset()
}
}
}
</script>

View File

@@ -0,0 +1,58 @@
<template>
<div class="row">
<div class="col-lg-8 m-auto px-4">
<h1 class="my-6">
Verify Email
</h1>
<form @submit.prevent="send" @keydown="form.onKeydown($event)">
<alert-success :form="form" :message="status" />
<!-- Email -->
<text-input name="email" :form="form" label="Email" :required="true" />
<!-- Submit Button -->
<div class="form-group row">
<div class="col-md-9 ml-md-auto">
<v-button :loading="form.busy">
Send Verification Link
</v-button>
</div>
</div>
</form>
</div>
</div>
</template>
<script>
import Form from 'vform'
import SeoMeta from '../../../mixins/seo-meta.js'
export default {
mixins: [SeoMeta],
middleware: 'guest',
data: () => ({
metaTitle: 'Verify Email',
status: '',
form: new Form({
email: ''
})
}),
created () {
if (this.$route.query.email) {
this.form.email = this.$route.query.email
}
},
methods: {
async send () {
const { data } = await this.form.post('/api/email/resend')
this.status = data.status
this.form.reset()
}
}
}
</script>

View File

@@ -0,0 +1,59 @@
<template>
<div class="row">
<div class="col-lg-8 m-auto px-4">
<h1 class="my-6">
Verify Email
</h1>
<template v-if="success">
<div class="alert alert-success" role="alert">
{{ success }}
</div>
<router-link :to="{ name: 'login' }" class="btn btn-primary">
Login
</router-link>
</template>
<template v-else>
<div class="alert alert-danger" role="alert">
{{ error || 'Failed to verify email.' }}
</div>
<router-link :to="{ name: 'verification.resend' }" class="small float-right">
Resend Verification Link?
</router-link>
</template>
</div>
</div>
</template>
<script>
import axios from 'axios'
import SeoMeta from '../../../mixins/seo-meta.js'
const qs = (params) => Object.keys(params).map(key => `${key}=${params[key]}`).join('&')
export default {
mixins: [SeoMeta],
async beforeRouteEnter (to, from, next) {
try {
const { data } = await axios.post(`/api/email/verify/${to.params.id}?${qs(to.query)}`)
next(vm => {
vm.success = data.status
})
} catch (e) {
next(vm => {
vm.error = e.response.data.status
})
}
},
middleware: 'guest',
data: () => ({
metaTitle: 'Verify Email',
error: '',
success: ''
})
}
</script>

View File

@@ -0,0 +1,35 @@
<template>
<div>
<div class="mt-6 flex flex-col">
<div class="w-full md:max-w-3xl md:mx-auto px-4 md:pt-16 pb-10">
<h1 class="sm:text-5xl mb-4">
OpnForm Discount for Students, Academics and NGOs
</h1>
<notion-page page-id="c65a499d39834e0b8978556a8d7af867" />
</div>
</div>
<open-form-footer />
</div>
</template>
<script>
import OpenFormFooter from '../../components/pages/OpenFormFooter.vue'
export default {
components: { OpenFormFooter },
layout: 'default',
props: {
metaTitle: { type: String, default: 'OpnForm Discount for Students, Academics and NGOs' },
metaDescription: { type: String, default: 'If you are a student, an academic of if you work for a NGO we are happy to offer you a 40% discount on your OpnForm Pro subscription.' }
},
data: () => ({
}),
computed: {},
mounted () {
}
}
</script>

View File

@@ -0,0 +1,24 @@
<template>
<div class="flex mt-6">
<div class="w-full md:w-2/3 md:mx-auto md:max-w-md">
<img alt="Nice plant as we have nothing else to show!" src="/img/icons/plant.png" class="w-56 mb-5">
<h1 class="mb-4 font-semibold text-3xl text-gray-900">
Page Not Found
</h1>
<div class="links">
<router-link :to="{ name: 'index' }" class="hover:underline text-gray-700">
Go Home
</router-link>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'NotFound'
}
</script>

View File

@@ -0,0 +1,153 @@
<template>
<div class="flex flex-wrap flex-col">
<transition v-if="stateReady" name="fade" mode="out-in">
<div key="2">
<create-form-base-modal :show="showInitialFormModal" @form-generated="formGenerated"
@close="showInitialFormModal=false"
/>
<form-editor v-if="!workspacesLoading" ref="editor"
class="w-full flex flex-grow"
:error="error"
:is-guest="isGuest"
@openRegister="openRegister"
/>
<div v-else class="text-center mt-4 py-6">
<loader class="h-6 w-6 text-nt-blue mx-auto" />
</div>
</div>
</transition>
<quick-register :show-register-modal="registerModal" @close="registerModal=false" @reopen="registerModal=true"
@afterLogin="afterLogin"
/>
</div>
</template>
<script>
import { computed } from 'vue'
import Form from 'vform'
import { useTemplatesStore } from '../../stores/templates'
import { useWorkingFormStore } from '../../stores/working_form'
import { useWorkspacesStore } from '../../stores/workspaces'
import QuickRegister from '~/components/pages/auth/components/QuickRegister.vue'
import initForm from '../../mixins/form_editor/initForm.js'
import SeoMeta from '../../mixins/seo-meta.js'
import CreateFormBaseModal from '../../components/pages/forms/create/CreateFormBaseModal.vue'
const loadTemplates = function () {
const templatesStore = useTemplatesStore()
templatesStore.startLoading()
templatesStore.loadIfEmpty().then(() => {
templatesStore.stopLoading()
})
}
export default {
name: 'CreateFormGuest',
components: {
QuickRegister, CreateFormBaseModal
},
mixins: [initForm, SeoMeta],
middleware: 'guest',
beforeRouteEnter (to, from, next) {
loadTemplates()
next()
},
setup () {
const templatesStore = useTemplatesStore()
const workingFormStore = useWorkingFormStore()
const workspacesStore = useWorkspacesStore()
return {
templatesStore,
workingFormStore,
workspacesStore,
workspaces : computed(() => workspacesStore.content),
workspacesLoading : computed(() => workspacesStore.loading)
}
},
data () {
return {
metaTitle: 'Create a new Form as Guest',
stateReady: false,
loading: false,
error: '',
registerModal: false,
isGuest: true,
showInitialFormModal: false
}
},
computed: {
form: {
get () {
return this.workingFormStore.content
},
/* We add a setter */
set (value) {
this.workingFormStore.set(value)
}
},
workspace () {
return this.workspacesStore.getCurrent()
}
},
watch: {
workspace () {
if (this.workspace) {
this.form.workspace_id = this.workspace.id
}
}
},
mounted () {
// Set as guest user
const guestWorkspace = {
id: null,
name: 'Guest Workspace',
is_enterprise: false,
is_pro: false
}
this.workspacesStore.set([guestWorkspace])
this.workspacesStore.setCurrentId(guestWorkspace.id)
this.initForm()
if (this.$route.query.template !== undefined && this.$route.query.template) {
const template = this.templatesStore.getBySlug(this.$route.query.template)
if (template && template.structure) {
this.form = new Form({ ...this.form.data(), ...template.structure })
}
} else {
// No template loaded, ask how to start
this.showInitialFormModal = true
}
this.closeAlert()
this.stateReady = true
},
created () {},
unmounted () {},
methods: {
openRegister () {
this.registerModal = true
},
afterLogin () {
this.registerModal = false
this.isGuest = false
this.workspacesStore.load()
setTimeout(() => {
if (this.$refs.editor) {
this.$refs.editor.saveFormCreate()
}
}, 500)
},
formGenerated (form) {
this.form = new Form({ ...this.form.data(), ...form })
}
}
}
</script>

View File

@@ -0,0 +1,164 @@
<template>
<div class="flex flex-wrap flex-col">
<transition v-if="stateReady" name="fade" mode="out-in">
<div key="2">
<create-form-base-modal :show="showInitialFormModal" @form-generated="formGenerated"
@close="showInitialFormModal=false"
/>
<form-editor v-if="!workspacesLoading" ref="editor"
class="w-full flex flex-grow"
:error="error"
@on-save="formInitialHash=null"
/>
<div v-else class="text-center mt-4 py-6">
<loader class="h-6 w-6 text-nt-blue mx-auto" />
</div>
</div>
</transition>
</div>
</template>
<script>
import Form from 'vform'
import { computed } from 'vue'
import { useAuthStore } from '../../stores/auth'
import { useTemplatesStore } from '../../stores/templates'
import { useWorkingFormStore } from '../../stores/working_form'
import { useWorkspacesStore } from '../../stores/workspaces'
import initForm from '../../mixins/form_editor/initForm.js'
import SeoMeta from '../../mixins/seo-meta.js'
import CreateFormBaseModal from '../../components/pages/forms/create/CreateFormBaseModal.vue'
const loadTemplates = function () {
const templatesStore = useTemplatesStore()
templatesStore.startLoading()
templatesStore.loadIfEmpty().then(() => {
templatesStore.stopLoading()
})
}
export default {
name: 'CreateForm',
components: { CreateFormBaseModal },
mixins: [initForm, SeoMeta],
middleware: 'auth',
beforeRouteEnter (to, from, next) {
loadTemplates()
next()
},
beforeRouteLeave (to, from, next) {
if (this.isDirty()) {
return this.alertConfirm('Changes you made may not be saved. Are you sure want to leave?', () => {
window.onbeforeunload = null
next()
}, () => {})
}
next()
},
setup () {
const authStore = useAuthStore()
const templatesStore = useTemplatesStore()
const workingFormStore = useWorkingFormStore()
const workspacesStore = useWorkspacesStore()
return {
templatesStore,
workingFormStore,
workspacesStore,
user: computed(() => authStore.user),
workspaces : computed(() => workspacesStore.content),
workspacesLoading : computed(() => workspacesStore.loading)
}
},
data () {
return {
metaTitle: 'Create a new Form',
stateReady: false,
loading: false,
error: '',
showInitialFormModal: false,
formInitialHash: null
}
},
computed: {
form: {
get () {
return this.workingFormStore.content
},
/* We add a setter */
set (value) {
this.workingFormStore.set(value)
}
},
workspace () {
return this.workspacesStore.getCurrent()
}
},
watch: {
workspace () {
if (this.workspace) {
this.form.workspace_id = this.workspace.id
}
},
user () {
this.stateReady = true
}
},
mounted () {
window.onbeforeunload = () => {
if (this.isDirty()) {
return false
}
}
this.initForm()
this.formInitialHash = this.hashString(JSON.stringify(this.form.data()))
if (this.$route.query.template !== undefined && this.$route.query.template) {
const template = this.templatesStore.getBySlug(this.$route.query.template)
if (template && template.structure) {
this.form = new Form({ ...this.form.data(), ...template.structure })
}
} else {
// No template loaded, ask how to start
this.showInitialFormModal = true
}
this.closeAlert()
this.workspacesStore.loadIfEmpty()
this.stateReady = this.user !== null
},
created () {},
unmounted () {},
methods: {
formGenerated (form) {
this.form = new Form({ ...this.form.data(), ...form })
},
isDirty () {
return !this.loading && this.formInitialHash && this.formInitialHash !== this.hashString(JSON.stringify(this.form.data()))
},
hashString (str, seed = 0) {
let h1 = 0xdeadbeef ^ seed
let h2 = 0x41c6ce57 ^ seed
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i)
h1 = Math.imul(h1 ^ ch, 2654435761)
h2 = Math.imul(h2 ^ ch, 1597334677)
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909)
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909)
return 4294967296 * (2097151 & h2) + (h1 >>> 0)
}
}
}
</script>

148
client/pages/forms/edit.vue Normal file
View File

@@ -0,0 +1,148 @@
<template>
<div class="w-full flex flex-col">
<form-editor v-if="pageLoaded" ref="editor"
:is-edit="true"
@on-save="formInitialHash=null"
/>
<div v-else-if="!loading && error" class="mt-4 rounded-lg max-w-xl mx-auto p-6 bg-red-100 text-red-500">
{{ error }}
</div>
<div v-else class="text-center mt-4 py-6">
<loader class="h-6 w-6 text-nt-blue mx-auto" />
</div>
</div>
</template>
<script>
import { computed } from 'vue'
import Form from 'vform'
import { useFormsStore } from '../../stores/forms'
import { useWorkingFormStore } from '../../stores/working_form'
import { useWorkspacesStore } from '../../stores/workspaces'
import Breadcrumb from '~/components/global/Breadcrumb.vue'
import SeoMeta from '../../mixins/seo-meta.js'
const loadForms = function () {
const formsStore = useFormsStore()
const workspacesStore = useWorkspacesStore()
formsStore.startLoading()
workspacesStore.loadIfEmpty().then(() => {
formsStore.load(workspacesStore.currentId)
})
}
export default {
name: 'EditForm',
components: { Breadcrumb },
mixins: [SeoMeta],
middleware: 'auth',
beforeRouteEnter (to, from, next) {
const formsStore = useFormsStore()
const workingFormStore = useWorkingFormStore()
if (!formsStore.getBySlug(to.params.slug)) {
loadForms()
}
workingFormStore.set(null) // Reset old working form
next()
},
beforeRouteLeave (to, from, next) {
if (this.isDirty()) {
return this.alertConfirm('Changes you made may not be saved. Are you sure want to leave?', () => {
window.onbeforeunload = null
next()
}, () => {})
}
next()
},
setup () {
const formsStore = useFormsStore()
const workingFormStore = useWorkingFormStore()
const workspacesStore = useWorkspacesStore()
return {
formsStore,
workingFormStore,
workspacesStore,
formsLoading : computed(() => formsStore.loading)
}
},
data () {
return {
loading: false,
error: null,
formInitialHash: null
}
},
computed: {
updatedForm: {
get () {
return this.workingFormStore.content
},
/* We add a setter */
set (value) {
this.workingFormStore.set(value)
}
},
form () {
return this.formsStore.getBySlug(this.$route.params.slug)
},
pageLoaded () {
return !this.loading && this.updatedForm !== null
},
metaTitle () {
return 'Edit ' + (this.form ? this.form.title : 'Your Form')
}
},
watch: {
form () {
this.updatedForm = new Form(this.form)
}
},
created () {},
unmounted () {},
mounted () {
window.onbeforeunload = () => {
if (this.isDirty()) {
return false
}
}
this.closeAlert()
if (!this.form) {
loadForms()
} else {
this.updatedForm = new Form(this.form)
this.formInitialHash = this.hashString(JSON.stringify(this.updatedForm.data()))
}
if (this.updatedForm && (!this.updatedForm.notification_settings || Array.isArray(this.updatedForm.notification_settings))) {
this.updatedForm.notification_settings = {}
}
},
methods: {
isDirty () {
return !this.loading && this.formInitialHash && this.formInitialHash !== this.hashString(JSON.stringify(this.updatedForm.data()))
},
hashString (str, seed = 0) {
let h1 = 0xdeadbeef ^ seed
let h2 = 0x41c6ce57 ^ seed
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i)
h1 = Math.imul(h1 ^ ch, 2654435761)
h2 = Math.imul(h2 ^ ch, 1597334677)
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909)
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909)
return 4294967296 * (2097151 & h2) + (h1 >>> 0)
}
}
}
</script>

View File

@@ -0,0 +1,221 @@
<template>
<div class="flex flex-col">
<div v-if="form && !isIframe && (form.logo_picture || form.cover_picture)">
<div v-if="form.cover_picture">
<div id="cover-picture" class="max-h-56 w-full overflow-hidden flex items-center justify-center">
<img alt="Form Cover Picture" :src="form.cover_picture" class="w-full">
</div>
</div>
<div v-if="form.logo_picture" class="w-full p-5 relative mx-auto"
:class="{'pt-20':!form.cover_picture, 'md:w-3/5 lg:w-1/2 md:max-w-2xl': form.width === 'centered', 'max-w-7xl': (form.width === 'full' && !isIframe) }"
>
<img alt="Logo Picture" :src="form.logo_picture"
:class="{'top-5':!form.cover_picture, '-top-10':form.cover_picture}"
class="w-20 h-20 object-contain absolute left-5 transition-all"
>
</div>
</div>
<div class="w-full mx-auto px-4"
:class="{'mt-6':!isIframe, 'md:w-3/5 lg:w-1/2 md:max-w-2xl': form && (form.width === 'centered'), 'max-w-7xl': (form && form.width === 'full' && !isIframe)}"
>
<div v-if="!formLoading && !form">
<h1 class="mt-6" v-text="'Whoops'" />
<p class="mt-6">
Unfortunately we could not find this form. It may have been deleted by it's author.
</p>
<p class="mb-10 mt-4">
<router-link :to="{name:'index'}">
Create your form for free with OpnForm
</router-link>
</p>
</div>
<div v-else-if="formLoading">
<p class="text-center mt-6 p-4">
<loader class="h-6 w-6 text-nt-blue mx-auto" />
</p>
</div>
<template v-else>
<div v-if="recordLoading">
<p class="text-center mt-6 p-4">
<loader class="h-6 w-6 text-nt-blue mx-auto" />
</p>
</div>
<open-complete-form v-show="!recordLoading" ref="open-complete-form" :form="form" class="mb-10"
@password-entered="passwordEntered"
/>
</template>
</div>
</div>
</template>
<script>
import axios from 'axios'
import { computed } from 'vue'
import { useFormsStore } from '../../stores/forms'
import { useRecordsStore } from '../../stores/records'
import OpenCompleteForm from '../../components/open/forms/OpenCompleteForm.vue'
import Cookies from 'js-cookie'
import sha256 from 'js-sha256'
import SeoMeta from '../../mixins/seo-meta.js'
const isFrame = window.location !== window.parent.location || window.frameElement
function handleDarkMode (form) {
// Dark mode
const body = document.body
if (form.dark_mode === 'dark') {
body.classList.add('dark')
} else if (form.dark_mode === 'light') {
body.classList.remove('dark')
} else if (form.dark_mode === 'auto' && isFrame) {
// Remove dark mode if embed in a notion basic site
let parentUrl
try {
parentUrl = window.location.ancestorOrigins[0]
} catch (e) {
parentUrl = (window.location !== window.parent.location)
? document.referrer
: document.location.href
}
if (parentUrl.includes('.notion.site')) {
body.classList.remove('dark')
}
}
}
function handleTransparentMode (form) {
const isFrame = window.location !== window.parent.location || window.frameElement
if (!isFrame || !form.transparent_background) return
const app = document.getElementById('app')
app.classList.remove('bg-white')
app.classList.remove('dark:bg-notion-dark')
app.classList.add('bg-transparent')
}
function loadForm (slug) {
const formsStore = useFormsStore()
if (formsStore.loading) return
formsStore.startLoading()
return axios.get('/api/forms/' + slug).then((response) => {
const form = response.data
formsStore.set([response.data])
// Custom code injection
if (form.custom_code) {
const scriptEl = document.createRange().createContextualFragment(form.custom_code)
document.head.append(scriptEl)
}
handleDarkMode(form)
handleTransparentMode(form)
formsStore.stopLoading()
}).catch(() => {
formsStore.stopLoading()
})
}
export default {
components: { OpenCompleteForm },
mixins: [SeoMeta],
beforeRouteEnter (to, from, next) {
if (window.$crisp) {
window.$crisp.push(['do', 'chat:hide'])
}
next()
},
beforeRouteLeave (to, from, next) {
if (window.$crisp) {
window.$crisp.push(['do', 'chat:show'])
}
next()
},
setup () {
const formsStore = useFormsStore()
const recordsStore = useRecordsStore()
return {
formsStore,
forms : computed(() => formsStore.content),
formLoading : computed(() => formsStore.loading),
recordLoading : computed(() => recordsStore.loading)
}
},
data () {
return {
submitted: false
}
},
mounted () {
loadForm(this.formSlug).then(() => {
if (this.isIframe) return
// Auto focus on first input
const visibleElements = []
document.querySelectorAll('input,button,textarea,[role="button"]').forEach(ele => {
if (ele.offsetWidth !== 0 || ele.offsetHeight !== 0) {
visibleElements.push(ele)
}
})
if (visibleElements.length > 0) {
visibleElements[0].focus()
}
})
},
methods: {
passwordEntered (password) {
Cookies.set('password-' + this.form.slug, sha256(password), { expires: 7, sameSite: 'None', secure: true })
loadForm(this.formSlug).then(() => {
if (this.form.is_password_protected) {
this.$refs['open-complete-form'].addPasswordError('Invalid password.')
}
})
}
},
computed: {
formSlug () {
return this.$route.params.slug
},
form () {
return this.formsStore.getBySlug(this.formSlug)
},
isIframe () {
return window.location !== window.parent.location || window.frameElement
},
metaTitle () {
if(this.form && this.form.is_pro && this.form.seo_meta.page_title) {
return this.form.seo_meta.page_title
}
return this.form ? this.form.title : 'Create beautiful forms'
},
metaTemplate () {
if (this.form && this.form.is_pro && this.form.seo_meta.page_title) {
// Disable template if custom SEO title
return '%s'
}
return null
},
metaDescription () {
if (this.form && this.form.is_pro && this.form.seo_meta.page_description) {
return this.form.seo_meta.page_description
}
return (this.form && this.form.description) ? this.form.description.substring(0, 160) : null
},
metaImage () {
if (this.form && this.form.is_pro && this.form.seo_meta.page_thumbnail) {
return this.form.seo_meta.page_thumbnail
}
return (this.form && this.form.cover_picture) ? this.form.cover_picture : null
},
metaTags () {
return (this.form && this.form.can_be_indexed) ? [] : [{ name: 'robots', content: 'noindex' }]
}
}
}
</script>

View File

@@ -0,0 +1,270 @@
<template>
<div class="bg-white">
<template v-if="form">
<div class="flex bg-gray-50">
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4">
<div class="pt-4 pb-0">
<a href="#" class="flex text-blue mb-2 font-semibold text-sm" @click.prevent="goBack">
<svg class="w-3 h-3 text-blue mt-1 mr-1" viewBox="0 0 6 10" fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M5 9L1 5L5 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
Go back
</a>
<div class="flex flex-wrap">
<h2 class="flex-grow text-gray-900 truncate">
{{ form.title }}
</h2>
<div class="flex">
<extra-menu :form="form" />
<v-button v-track.view_form_click="{form_id:form.id, form_slug:form.slug}" target="_blank"
:href="form.share_url" color="white"
class="mr-2 text-blue-600 hidden sm:block"
>
<svg class="w-6 h-6 inline -mt-1" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M1 12C1 12 5 4 12 4C19 4 23 12 23 12C23 12 19 20 12 20C5 20 1 12 1 12Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
/>
<path
d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
/>
</svg>
</v-button>
<v-button class="text-white" @click="openEdit">
<svg class="inline mr-1 -mt-1" width="18" height="17" viewBox="0 0 18 17" fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.99998 15.6662H16.5M1.5 15.6662H2.89545C3.3031 15.6662 3.50693 15.6662 3.69874 15.6202C3.8688 15.5793 4.03138 15.512 4.1805 15.4206C4.34869 15.3175 4.49282 15.1734 4.78107 14.8852L15.25 4.4162C15.9404 3.72585 15.9404 2.60656 15.25 1.9162C14.5597 1.22585 13.4404 1.22585 12.75 1.9162L2.28105 12.3852C1.9928 12.6734 1.84867 12.8175 1.7456 12.9857C1.65422 13.1348 1.58688 13.2974 1.54605 13.4675C1.5 13.6593 1.5 13.8631 1.5 14.2708V15.6662Z"
stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"
/>
</svg>
Edit form
</v-button>
</div>
</div>
<p class="text-gray-500 text-sm">
<span class="pr-1">{{ form.views_count }} view{{ form.views_count > 0 ? 's' : '' }}</span>
<span class="pr-1">- {{ form.submissions_count }}
submission{{ form.submissions_count > 0 ? 's' : '' }}
</span>
<span>- Edited {{ form.last_edited_human }}</span>
</p>
<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"
>
{{ tag }}
</span>
</div>
<p v-if="form.closes_at" class="text-yellow-500">
<span v-if="form.is_closed"> This form stopped accepting submissions on the {{
displayClosesDate
}} </span>
<span v-else> This form will stop accepting submissions on the {{ displayClosesDate }} </span>
</p>
<p v-if="form.max_submissions_count > 0" class="text-yellow-500">
<span v-if="form.max_number_of_submissions_reached"> The form is now closed because it reached its limit of {{
form.max_submissions_count
}} submissions. </span>
<span v-else> This form will stop accepting submissions after {{ form.max_submissions_count }} submissions. </span>
</p>
<form-cleanings class="mt-4" :form="form" />
<div class="border-b border-gray-200 dark:border-gray-700">
<ul class="flex flex-wrap -mb-px text-sm font-medium text-center">
<li v-for="(tab, i) in tabsList" :key="i+1" class="mr-6">
<router-link :to="{ name: tab.route }"
class="hover:no-underline inline-block py-4 rounded-t-lg border-b-2 text-gray-500 hover:text-gray-600"
active-class="text-blue-600 hover:text-blue-900 dark:text-blue-500 dark:hover:text-blue-500 border-blue-600 dark:border-blue-500"
>
{{ tab.name }}
</router-link>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="flex bg-white">
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4">
<div class="py-4">
<router-view v-slot="{ Component }">
<transition name="page" mode="out-in">
<component :is="Component" :form="form" />
</transition>
</router-view>
</div>
</div>
</div>
</template>
<div v-else-if="loading" class="text-center w-full p-5">
<loader class="h-6 w-6 mx-auto" />
</div>
<div v-else class="text-center w-full p-5">
Form not found.
</div>
</div>
</template>
<script>
import { computed } from 'vue'
import Form from 'vform'
import { useAuthStore } from '../../../stores/auth'
import { useFormsStore } from '../../../stores/forms'
import { useWorkingFormStore } from '../../../stores/working_form'
import { useWorkspacesStore } from '../../../stores/workspaces'
import ProTag from '~/components/global/ProTag.vue'
import VButton from '~/components/global/VButton.vue'
import ExtraMenu from '../../../components/pages/forms/show/ExtraMenu.vue'
import SeoMeta from '../../../mixins/seo-meta.js'
import FormCleanings from '../../../components/pages/forms/show/FormCleanings.vue'
const loadForms = function () {
const formsStore = useFormsStore()
const workspacesStore = useWorkspacesStore()
formsStore.startLoading()
workspacesStore.loadIfEmpty().then(() => {
formsStore.loadIfEmpty(workspacesStore.currentId)
})
}
export default {
name: 'ShowForm',
components: {
VButton,
ProTag,
ExtraMenu,
FormCleanings
},
mixins: [SeoMeta],
beforeRouteEnter (to, from, next) {
loadForms()
next()
},
beforeRouteLeave (to, from, next) {
this.workingForm = null
next()
},
middleware: 'auth',
setup () {
const authStore = useAuthStore()
const formsStore = useFormsStore()
const workingFormStore = useWorkingFormStore()
const workspacesStore = useWorkspacesStore()
return {
formsStore,
workingFormStore,
workspacesStore,
user: computed(() => authStore.user),
formsLoading: computed(() => formsStore.loading),
workspacesLoading: computed(() => workspacesStore.loading)
}
},
data () {
return {
metaTitle: 'Home',
tabsList: [
{
name: 'Submissions',
route: 'forms.show'
},
{
name: 'Analytics',
route: 'forms.show.analytics'
},
{
name: 'Share',
route: 'forms.show.share'
}
]
}
},
computed: {
workingForm: {
get () {
return this.workingFormStore.content
},
/* We add a setter */
set (value) {
this.workingFormStore.set(value)
}
},
workspace () {
if (!this.form) return null
return this.workspacesStore.getById(this.form.workspace_id)
},
form () {
return this.formsStore.getBySlug(this.$route.params.slug)
},
formEndpoint: () => '/api/open/forms/{id}',
loading () {
return this.formsLoading || this.workspacesLoading
},
displayClosesDate () {
if (this.form.closes_at) {
const dateObj = new Date(this.form.closes_at)
return dateObj.getFullYear() + '-' +
String(dateObj.getMonth() + 1).padStart(2, '0') + '-' +
String(dateObj.getDate()).padStart(2, '0') + ' ' +
String(dateObj.getHours()).padStart(2, '0') + ':' +
String(dateObj.getMinutes()).padStart(2, '0')
}
return ''
}
},
watch: {
form () {
this.workingForm = new Form(this.form)
}
},
mounted () {
if (this.form) {
this.workingForm = new Form(this.form)
}
},
methods: {
openCrisp () {
window.$crisp.push(['do', 'chat:show'])
window.$crisp.push(['do', 'chat:open'])
},
goBack () {
this.$router.push({ name: 'home' })
},
openEdit () {
this.$router.push({ name: 'forms.edit', params: { slug: this.form.slug } })
}
}
}
</script>

View File

@@ -0,0 +1,70 @@
<template>
<div>
<share-link class="mt-4" :form="form" :extra-query-param="shareUrlForQueryParams" />
<embed-code class="mt-6" :form="form" :extra-query-param="shareUrlForQueryParams" />
<form-qr-code class="mt-6" :form="form" :extra-query-param="shareUrlForQueryParams" />
<advanced-form-url-settings :form="form" v-model="shareFormConfig" />
<div class="mt-6 pt-6 border-t w-full flex">
<regenerate-form-link class="sm:w-1/2 mr-4" :form="form" />
<url-form-prefill class="sm:w-1/2 mr-4" :form="form" :extra-query-param="shareUrlForQueryParams" />
<embed-form-as-popup-modal class="sm:w-1/2" :form="form" />
</div>
</div>
</template>
<script>
import ShareLink from '../../../components/pages/forms/show/ShareLink.vue'
import EmbedCode from '../../../components/pages/forms/show/EmbedCode.vue'
import FormQrCode from '../../../components/pages/forms/show/FormQrCode.vue'
import UrlFormPrefill from '../../../components/pages/forms/show/UrlFormPrefill.vue'
import RegenerateFormLink from '../../../components/pages/forms/show/RegenerateFormLink.vue'
import SeoMeta from '../../../mixins/seo-meta.js'
import AdvancedFormUrlSettings from '../../../components/open/forms/components/AdvancedFormUrlSettings.vue'
import EmbedFormAsPopupModal from '../../../components/pages/forms/show/EmbedFormAsPopupModal.vue'
export default {
components: {
ShareLink,
EmbedCode,
FormQrCode,
UrlFormPrefill,
RegenerateFormLink,
AdvancedFormUrlSettings,
EmbedFormAsPopupModal
},
props: {
form: { type: Object, required: true }
},
mixins: [SeoMeta],
data: () => ({
shareFormConfig: {
hide_title: false,
auto_submit: false
}
}),
mounted() {},
computed: {
metaTitle() {
return (this.form) ? 'Form Share - '+this.form.title : 'Form Share'
},
shareUrlForQueryParams () {
let queryStr = ''
for (const [key, value] of Object.entries(this.shareFormConfig)) {
if(value && value !== 'false' && value !== false){
queryStr += '&' + encodeURIComponent(key) + "=" + encodeURIComponent(value)
}
}
return queryStr.slice(1)
}
},
}
</script>

View File

@@ -0,0 +1,30 @@
<template>
<div>
<h3 class="font-semibold mt-4 text-xl">
Form Analytics (last 30 days)
</h3>
<form-stats :form="form"/>
</div>
</template>
<script>
import FormStats from '../../../components/open/forms/components/FormStats.vue'
import SeoMeta from '../../../mixins/seo-meta.js'
export default {
name: 'Stats',
components: {FormStats},
props: {
form: {type: Object, required: true},
},
mixins: [SeoMeta],
data: () => ({}),
computed: {
metaTitle() {
return (this.form ? ('Form Analytics - ' + this.form.title) : 'Form Analytics')
}
}
}
</script>

View File

@@ -0,0 +1,31 @@
<template>
<div id="table-page">
<form-submissions/>
</div>
</template>
<script>
import FormSubmissions from '../../../components/open/forms/components/FormSubmissions.vue'
import SeoMeta from '../../../mixins/seo-meta.js'
export default {
components: {FormSubmissions},
props: {
form: {type: Object, required: true}
},
mixins: [SeoMeta],
data: () => ({}),
mounted() {
},
computed: {
metaTitle() {
return (this.form) ? 'Form Submissions - ' + this.form.title : 'Form Submissions'
},
},
methods: {}
}
</script>

224
client/pages/home.vue Normal file
View File

@@ -0,0 +1,224 @@
<template>
<div class="bg-white">
<div class="flex bg-gray-50 pb-5 border-b">
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl p-4">
<div class="pt-4 pb-0">
<div class="flex">
<h2 class="flex-grow text-gray-900">
Your Forms
</h2>
<v-button v-track.create_form_click :to="{name:'forms.create'}">
<svg class="w-4 h-4 text-white inline mr-1 -mt-1" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.99996 1.1665V12.8332M1.16663 6.99984H12.8333" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round" />
</svg>
Create a new form
</v-button>
</div>
<small class="flex text-gray-500">Manage your forms and submissions.</small>
</div>
</div>
</div>
<div class="flex bg-white">
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4">
<div class="mt-8 pb-0">
<text-input v-if="forms.length > 0" class="mb-6" :form="searchForm" name="search" label="Search a form"
placeholder="Name of form to search"
/>
<div v-if="allTags.length > 0" class="mb-4">
<div v-for="tag in allTags" :key="tag"
:class="[
'inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ring-1 ring-inset cursor-pointer mr-2',
{'bg-blue-50 text-blue-600 ring-blue-500/10 dark:bg-blue-400':selectedTags.includes(tag),
'bg-gray-50 text-gray-600 ring-gray-500/10 dark:bg-gray-700 hover:bg-blue-50 hover:text-blue-600 hover:ring-blue-500/10 hover:dark:bg-blue-400':!selectedTags.includes(tag)}
]"
title="Click for filter by tag(s)"
@click="onTagClick(tag)"
>
{{ tag }}
</div>
</div>
<div v-if="!formsLoading && enrichedForms.length === 0" class="flex flex-wrap justify-center max-w-4xl">
<img loading="lazy" class="w-56"
src="/img/pages/forms/search_notfound.png" alt="search-not-found"
>
<h3 class="w-full mt-4 text-center text-gray-900 font-semibold">
No forms found
</h3>
<div v-if="isFilteringForms && enrichedForms.length === 0 && searchForm.search" class="mt-2 w-full text-center">
Your search "{{ searchForm.search }}" did not match any forms. Please try again.
</div>
<v-button v-if="forms.length === 0" v-track.create_form_click class="mt-4" :to="{name:'forms.create'}">
<svg class="w-4 h-4 text-white inline mr-1 -mt-1" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.99996 1.1665V12.8332M1.16663 6.99984H12.8333" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round" />
</svg>
Create a new form
</v-button>
</div>
<div v-else-if="forms.length > 0" class="mb-10">
<div v-if="enrichedForms && enrichedForms.length">
<div v-for="(form) in enrichedForms" :key="form.id"
class="mt-4 p-4 flex group bg-white hover:bg-gray-50 dark:bg-notion-dark items-center"
>
<div class="flex-grow items-center truncate cursor-pointer" role="button" @click.prevent="viewForm(form)">
<span class="font-semibold text-gray-900 dark:text-white">{{ form.title }}</span>
<ul class="flex text-gray-500">
<li class="pr-1">
{{ form.views_count }} view{{ form.views_count > 0 ? 's' : '' }}
</li>
<li class="list-disc ml-6 pr-1">
{{ form.submissions_count }}
submission{{ form.submissions_count > 0 ? 's' : '' }}
</li>
<li class="list-disc ml-6">
Edited {{ form.last_edited_human }}
</li>
</ul>
<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"
>
{{ tag }}
</span>
</div>
</div>
<extra-menu :form="form" :is-main-page="true" />
</div>
</div>
</div>
<div v-if="formsLoading" class="text-center">
<loader class="h-6 w-6 text-nt-blue mx-auto" />
</div>
</div>
</div>
</div>
<open-form-footer class="mt-8 border-t" />
</div>
</template>
<script>
import { computed } from 'vue'
import { useAuthStore } from '../stores/auth';
import { useFormsStore } from '../stores/forms';
import { useWorkspacesStore } from '../stores/workspaces';
import Fuse from 'fuse.js'
import Form from 'vform'
import TextInput from '../components/forms/TextInput.vue'
import OpenFormFooter from '../components/pages/OpenFormFooter.vue'
import ExtraMenu from '../components/pages/forms/show/ExtraMenu.vue'
const loadForms = function () {
const formsStore = useFormsStore()
const workspacesStore = useWorkspacesStore()
formsStore.startLoading()
workspacesStore.loadIfEmpty().then(() => {
formsStore.loadIfEmpty(workspacesStore.currentId)
})
}
export default {
components: { OpenFormFooter, TextInput, ExtraMenu },
beforeRouteEnter (to, from, next) {
loadForms()
next()
},
middleware: 'auth',
props: {
metaTitle: { type: String, default: 'Your Forms' },
metaDescription: { type: String, default: 'All of your OpnForm are here. Create new forms, or update your existing one!' }
},
setup () {
const authStore = useAuthStore()
const formsStore = useFormsStore()
const workspacesStore = useWorkspacesStore()
return {
formsStore,
workspacesStore,
user : computed(() => authStore.user),
forms : computed(() => formsStore.content),
formsLoading : computed(() => formsStore.loading)
}
},
data () {
return {
showEditFormModal: false,
selectedForm: null,
searchForm: new Form({
search: ''
}),
selectedTags: []
}
},
mounted () {},
methods: {
editForm (form) {
this.selectedForm = form
this.showEditFormModal = true
},
onTagClick (tag) {
const idx = this.selectedTags.indexOf(tag)
if (idx === -1) {
this.selectedTags.push(tag)
} else {
this.selectedTags.splice(idx, 1)
}
},
viewForm (form) {
this.$router.push({ name: 'forms.show', params: { slug: form.slug } })
}
},
computed: {
isFilteringForms () {
return (this.searchForm.search !== '' && this.searchForm.search !== null) || this.selectedTags.length > 0
},
enrichedForms () {
let enrichedForms = this.forms.map((form) => {
form.workspace = this.workspacesStore.getById(form.workspace_id)
return form
})
// Filter by Selected Tags
if (this.selectedTags.length > 0) {
enrichedForms = enrichedForms.filter((item) => {
return (item.tags && item.tags.length > 0) ? this.selectedTags.every(r => item.tags.includes(r)) : false
})
}
if (!this.isFilteringForms || this.searchForm.search === '' || this.searchForm.search === null) {
return enrichedForms
}
// Fuze search
const fuzeOptions = {
keys: [
'title',
'slug',
'tags'
]
}
const fuse = new Fuse(enrichedForms, fuzeOptions)
return fuse.search(this.searchForm.search).map((res) => {
return res.item
})
},
allTags () {
return this.formsStore.getAllTags
}
}
}
</script>

245
client/pages/index.vue Normal file
View File

@@ -0,0 +1,245 @@
<template>
<div>
<section class="bg-gradient-to-b relative from-white to-gray-100 py-8 sm:py-16 ">
<div class="absolute inset-0">
<img class="w-full h-full object-cover object-top"
src="/img/pages/ai_form_builder/background-pattern.svg" alt=""
>
</div>
<div class="px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto relative -mb-32 md:-mb-52 lg:-mb-72">
<div class="flex justify-center mb-5">
<div
class="relative flex items-center shadow-sm bg-white gap-x-4 rounded-full px-4 py-1 text-sm leading-6 text-gray-600 ring-1 ring-gray-900/10 hover:ring-gray-900/20"
>
<span class="font-semibold text-gray-500">We're Open-Source</span><span class="h-4 w-px bg-gray-900/10"
aria-hidden="true"
/>
<a
target="_blank" class="flex items-center gap-x-1 hover:no-underline"
href="https://github.com/jhumanj/opnform" v-track.welcome_github_click
>
<span class="absolute inset-0" aria-hidden="true"/>
Star us on GitHub
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" aria-hidden="true" class="-mr-2 h-5 w-5 text-gray-400"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"/>
</svg>
</a>
</div>
</div>
<div class="max-w-4xl mx-auto text-center">
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-semibold text-gray-900 tracking-tight">
Build
<span
class="bg-clip-text text-transparent bg-gradient-to-r from-blue-600 to-blue-400">beautiful forms</span>
<br>
in seconds
</h1>
<p class="mt-4 sm:mt-5 text-base leading-7 sm:text-xl sm:leading-9 font-medium text-gray-500">
Create beautiful forms and share them anywhere. It super fast, you don't need to know how to code. Get
started
<span class="font-semibold">for free</span>!
</p>
<div class="mt-8 flex justify-center">
<!-- <v-button v-if="!authenticated" class="mr-1" :to="{ name: 'forms.create.guest' }" :arrow="true">-->
<!-- Create a form for FREE-->
<!-- </v-button>-->
<!-- <v-button v-else class="mr-1" :to="{ name: 'forms.create' }" :arrow="true">-->
<!-- Create a form for FREE-->
<!-- </v-button>-->
</div>
<div class="justify-center flex gap-2 mt-10">
<div class="flex items-center text-gray-400 text-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" class="w-4 h-4 mr-1 ticks"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/>
</svg>
<span>Unlimited forms</span>
</div>
<div class="flex items-center text-gray-400 text-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" class="w-4 h-4 mr-1 ticks"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/>
</svg>
<span>
Unlimited fields
</span>
</div>
<div class="flex text-gray-400 text-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" class="w-4 h-4 mr-1 ticks"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/>
</svg>
<span>Unlimited responses</span>
</div>
</div>
</div>
<div
class="w-full mt-12 relative px-6 mx-auto max-w-4xl sm:px-10 lg:px-0 z-10 flex items-center justify-center"
>
<div
class="-m-2 rounded-xl bg-blue-900/5 p-2 backdrop-blur-sm ring-1 ring-inset ring-blue-900/10 lg:-m-4 lg:rounded-2xl lg:p-4"
>
<img src="/img/pages/welcome/product-cover.jpg"
alt="Product screenshot" loading="lazy" class="rounded-md shadow-2xl ring-1 ring-gray-900/10"
>
</div>
</div>
</div>
</section>
<div class="flex flex-col bg-gray-50 dark:bg-notion-dark">
<div class="bg-white dark:bg-notion-dark-light pt-32 md:pt-52 lg:pt-72 pb-8">
<div class="md:max-w-5xl md:mx-auto w-full">
<features class="pb-8"/>
</div>
</div>
<ai-feature class="bg-white -mb-56"/>
<more-features class="pt-56"/>
<pricing-table v-if="paidPlansEnabled" class="pb-20" :home-page="true">
<template #pricing-table>
<li class="flex gap-x-3">
<router-link :to="{name:'pricing'}" class="flex gap-3">
<div class="w-5"/>
Read more about our pricing
</router-link>
</li>
</template>
</pricing-table>
<!-- <div class="pt-20 pb-5 text-center bg-white dark:bg-notion-dark-light">-->
<!-- <h3 class="font-semibold text-3xl">See what people are saying</h3>-->
<!-- <p class="w-full mt-2 mb-8">-->
<!-- These are the stories of our customers who have joined us with great pleasure when using this crazy feature.-->
<!-- </p>-->
<!-- <testimonials/>-->
<!-- </div>-->
<!-- <templates-slider class="max-w-full mb-12"/>-->
<div class="w-full bg-blue-900 p-12 md:p-24 text-center">
<h4 class="font-semibold text-3xl text-white">
Take your forms to the next level
</h4>
<p class="text-gray-300 my-8">
Generous, unlimited free plan.
</p>
<div class="mt-6 flex justify-center">
<!-- <v-button v-track.welcome_create_form_click :to="{ name: 'forms.create.guest' }" :arrow="true" color="blue">-->
<!-- Create a form for FREE-->
<!-- </v-button>-->
</div>
<div class="flex justify-center mt-6">
<a target="_blank" :href="configLinks.twitter" class="mr-4">
<svg class="w-6 h-6 text-white" viewBox="0 0 24 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M7.55016 19.7502C16.6045 19.7502 21.5583 12.2469 21.5583 5.74211C21.5583 5.53117 21.5536 5.31554 21.5442 5.1046C22.5079 4.40771 23.3395 3.5445 24 2.55554C23.1025 2.95484 22.1496 3.21563 21.1739 3.32898C22.2013 2.71315 22.9705 1.74572 23.3391 0.606011C22.3726 1.1788 21.3156 1.58286 20.2134 1.80085C19.4708 1.01181 18.489 0.48936 17.4197 0.314295C16.3504 0.13923 15.2532 0.321295 14.2977 0.832341C13.3423 1.34339 12.5818 2.15495 12.1338 3.14156C11.6859 4.12816 11.5754 5.23486 11.8195 6.29054C9.86249 6.19233 7.94794 5.68395 6.19998 4.79834C4.45203 3.91274 2.90969 2.66968 1.67297 1.14976C1.0444 2.23349 0.852057 3.51589 1.13503 4.73634C1.418 5.95678 2.15506 7.02369 3.19641 7.72023C2.41463 7.69541 1.64998 7.48492 0.965625 7.10617V7.1671C0.964925 8.30439 1.3581 9.40683 2.07831 10.287C2.79852 11.1672 3.80132 11.7708 4.91625 11.9952C4.19206 12.1934 3.43198 12.2222 2.69484 12.0796C3.00945 13.0577 3.62157 13.9131 4.44577 14.5266C5.26997 15.14 6.26512 15.4808 7.29234 15.5015C5.54842 16.8714 3.39417 17.6144 1.17656 17.6109C0.783287 17.6103 0.390399 17.5861 0 17.5387C2.25286 18.984 4.87353 19.7516 7.55016 19.7502Z"
fill="currentColor"
/>
</svg>
</a>
<a target="_blank" :href="configLinks.facebook_group" class="mr-4">
<svg class="w-6 h-6 text-white" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 17.9895 4.3882 22.954 10.125 23.8542V15.4688H7.07812V12H10.125V9.35625C10.125 6.34875 11.9166 4.6875 14.6576 4.6875C15.9701 4.6875 17.3438 4.92188 17.3438 4.92188V7.875H15.8306C14.34 7.875 13.875 8.80008 13.875 9.75V12H17.2031L16.6711 15.4688H13.875V23.8542C19.6118 22.954 24 17.9895 24 12Z"
fill="currentColor"
/>
</svg>
</a>
<a v-track.welcome_github_click target="_blank" :href="configLinks.github_url" class="mr-4">
<svg class="w-6 h-6 text-white" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M12 0C5.3724 0 0 5.3808 0 12.0204C0 17.3304 3.438 21.8364 8.2068 23.4252C8.8068 23.5356 9.0252 23.1648 9.0252 22.8456C9.0252 22.5612 9.0156 21.804 9.0096 20.802C5.6712 21.528 4.9668 19.1904 4.9668 19.1904C4.422 17.8008 3.6348 17.4312 3.6348 17.4312C2.5452 16.6872 3.7176 16.7016 3.7176 16.7016C4.9212 16.7856 5.5548 17.94 5.5548 17.94C6.6252 19.776 8.364 19.2456 9.0468 18.9384C9.1572 18.162 9.4668 17.6328 9.81 17.3328C7.146 17.0292 4.344 15.9972 4.344 11.3916C4.344 10.08 4.812 9.006 5.5788 8.166C5.4552 7.8624 5.0436 6.6396 5.6964 4.986C5.6964 4.986 6.7044 4.662 8.9964 6.2172C9.97532 5.95022 10.9853 5.81423 12 5.8128C13.02 5.8176 14.046 5.9508 15.0048 6.2172C17.2956 4.662 18.3012 4.9848 18.3012 4.9848C18.9564 6.6396 18.5436 7.8624 18.4212 8.166C19.1892 9.006 19.6548 10.08 19.6548 11.3916C19.6548 16.0092 16.848 17.0256 14.1756 17.3232C14.6064 17.694 14.9892 18.4272 14.9892 19.5492C14.9892 21.1548 14.9748 22.452 14.9748 22.8456C14.9748 23.1672 15.1908 23.5416 15.8004 23.424C18.19 22.6225 20.2672 21.0904 21.7386 19.0441C23.2099 16.9977 24.001 14.5408 24 12.0204C24 5.3808 18.6264 0 12 0Z"
fill="currentColor"
/>
</svg>
</a>
</div>
<p class="mt-12 text-white text-lg">
The form below is an OpnForm, give it a try !
</p>
<div class="md:max-w-5xl md:mx-auto w-full bg-white rounded-md mt-6 p-4 shadow-lg">
<iframe class="mt-4" style="border:none;width:100%;" height="470px"
src="https://opnform.com/forms/opnform-contact"
/>
</div>
</div>
<open-form-footer class="dark:border-t border-t"/>
</div>
</div>
</template>
<script>
import {computed} from 'vue'
import {useAuthStore} from '../stores/auth'
import Features from '~/components/pages/welcome/Features.vue'
import MoreFeatures from '~/components/pages/welcome/MoreFeatures.vue'
import PricingTable from '../components/pages/pricing/PricingTable.vue'
import AiFeature from '~/components/pages/welcome/AiFeature.vue'
import OpenFormFooter from '../components/pages/OpenFormFooter.vue'
import Testimonials from '../components/pages/welcome/Testimonials.vue'
import TemplatesSlider from '../components/pages/welcome/TemplatesSlider.vue'
import SeoMeta from '../mixins/seo-meta.js'
export default {
components: {Testimonials, OpenFormFooter, Features, MoreFeatures, PricingTable, AiFeature, TemplatesSlider},
mixins: [SeoMeta],
layout: 'default',
setup() {
const authStore = useAuthStore()
return {
authenticated: computed(() => authStore.check),
config: useConfig()
}
},
data: () => ({
title: 'OpnForm',
metaTitle: 'Create beautiful & open-source forms for free'
}),
computed: {
configLinks() {
return this.config.links
},
paidPlansEnabled() {
return this.config.paid_plans_enabled
},
}
}
</script>
<style lang="scss" scoped>
.customer-logo-container {
max-width: 130px;
width: 100%;
}
.ticks {
color: #2563eb;
}
@screen md {
#macbook-video {
position: absolute;
max-width: 84.8% !important;
right: 0px;
top: 6.8%;
}
}
</style>

View File

@@ -0,0 +1,43 @@
<template>
<div>
<div class="mt-6 flex flex-col">
<div class="w-full md:max-w-3xl md:mx-auto px-4 md:pt-16">
<h1 class="sm:text-5xl mb-4">
Integrations
</h1>
<NotionPage class="mb-8 integration-page" page-id="492c2bbb31404481b9faaaf407e59640" />
</div>
</div>
<open-form-footer />
</div>
</template>
<script>
import OpenFormFooter from '../components/pages/OpenFormFooter.vue'
export default {
components: { OpenFormFooter },
layout: 'default',
props: {
metaTitle: { type: String, default: 'Integrations' },
metaDescription: { type: String, default: 'You can connect your OpnForms to other services via our two integrations: Zapier and Webhooks. Use our integrations to automate your various workflows.' }
},
data: () => ({
}),
computed: {},
mounted () {
}
}
</script>
<style lang="scss">
.integration-page {
.notion-asset-wrapper {
max-width: 200px;
}
}
</style>

83
client/pages/login.vue Normal file
View File

@@ -0,0 +1,83 @@
<template>
<div>
<div class="flex mt-6 mb-10">
<div class="w-full md:max-w-6xl mx-auto px-4 flex md:flex-row-reverse flex-wrap">
<div class="w-full md:w-1/2 md:p-6">
<div class="border rounded-md p-6 shadow-md sticky top-4">
<h2 class="font-semibold text-2xl">
Login to OpnForm
</h2>
<small>Welcome back! Please enter your details.</small>
<login-form />
</div>
</div>
<div class="w-full md:w-1/2 md:p-6 mt-8 md:mt-0 ">
<h1 class="font-bold">
Create beautiful forms and share them anywhere
</h1>
<p class="text-gray-900 my-4 text-lg">
It takes seconds, you don't need to know how to code and it's free.
</p>
<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"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
Unlimited forms
</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"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
Unlimited fields
</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"
>
<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>
</div>
</div>
<open-form-footer />
</div>
</template>
<script>
import OpenFormFooter from '../../components/pages/OpenFormFooter.vue'
import Testimonials from '../../components/pages/welcome/Testimonials.vue'
import LoginForm from './components/LoginForm.vue'
import SeoMeta from '../../mixins/seo-meta.js'
export default {
components: {
OpenFormFooter,
Testimonials,
LoginForm
},
middleware: 'guest',
mixins: [SeoMeta],
data: () => ({
metaTitle: 'Login',
}),
methods: {
}
}
</script>

284
client/pages/pricing.vue Normal file
View File

@@ -0,0 +1,284 @@
<template>
<div>
<section class="relative py-12 bg-gradient-to-b from-white to-gray-100 sm:py-16 lg:py-20 xl:py-24">
<div class="relative px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl">
<div class="max-w-4xl mx-auto text-center">
<h1 class="text-4xl font-semibold tracking-tight text-gray-900 sm:text-5xl lg:text-6xl">
Simple, transparent pricing. No surprises.
</h1>
<p
class="max-w-2xl mx-auto mt-4 text-base font-medium leading-7 text-gray-500 sm:mt-5 sm:text-xl sm:leading-9">
Just like our codebase, our pricing is 100% transparent. One flat price for all features. No hidden fees.
</p>
</div>
</div>
</section>
<pricing-table/>
<section class="py-12 bg-white sm:py-16 lg:py-24 xl:py-24">
<div class="px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl">
<div class="max-w-2xl mx-auto text-center">
<h2 class="text-3xl font-semibold tracking-tight text-gray-900 sm:text-4xl lg:leading-tight">
<span class="text-blue-600">99%</span> of features are available to all users for free and without
limits.
</h2>
</div>
<div
class="grid max-w-5xl grid-cols-2 mx-auto mt-12 text-center gap-y-8 gap-x-4 sm:grid-cols-3 md:gap-x-12 md:text-left sm:mt-16">
<div class="flex flex-col items-center gap-3 md:flex-row">
<svg aria-hidden="true" class="w-6 h-6 shrink-0 stroke-blue-600" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M3 9H21M7.8 3H16.2C17.8802 3 18.7202 3 19.362 3.32698C19.9265 3.6146 20.3854 4.07354 20.673 4.63803C21 5.27976 21 6.11984 21 7.8V16.2C21 17.8802 21 18.7202 20.673 19.362C20.3854 19.9265 19.9265 20.3854 19.362 20.673C18.7202 21 17.8802 21 16.2 21H7.8C6.11984 21 5.27976 21 4.63803 20.673C4.07354 20.3854 3.6146 19.9265 3.32698 19.362C3 18.7202 3 17.8802 3 16.2V7.8C3 6.11984 3 5.27976 3.32698 4.63803C3.6146 4.07354 4.07354 3.6146 4.63803 3.32698C5.27976 3 6.11984 3 7.8 3Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
<p class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950">
Unlimited forms
</p>
</div>
<div class="flex flex-col items-center gap-3 md:flex-row">
<svg aria-hidden="true" class="w-6 h-6 shrink-0 stroke-blue-600" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M9 3.5V2M5.06066 5.06066L4 4M5.06066 13L4 14.0607M13 5.06066L14.0607 4M3.5 9H2M8.5 8.5L12.6111 21.2778L15.5 18.3889L19.1111 22L22 19.1111L18.3889 15.5L21.2778 12.6111L8.5 8.5Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
<p class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950">
Unlimited submissions
</p>
</div>
<div class="flex flex-col items-center gap-3 md:flex-row">
<svg aria-hidden="true" class="w-6 h-6 shrink-0 stroke-blue-600" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M17.8 10C18.9201 10 19.4802 10 19.908 9.78201C20.2843 9.59027 20.5903 9.28431 20.782 8.90798C21 8.48016 21 7.92011 21 6.8V6.2C21 5.0799 21 4.51984 20.782 4.09202C20.5903 3.7157 20.2843 3.40973 19.908 3.21799C19.4802 3 18.9201 3 17.8 3L6.2 3C5.0799 3 4.51984 3 4.09202 3.21799C3.71569 3.40973 3.40973 3.71569 3.21799 4.09202C3 4.51984 3 5.07989 3 6.2L3 6.8C3 7.9201 3 8.48016 3.21799 8.90798C3.40973 9.28431 3.71569 9.59027 4.09202 9.78201C4.51984 10 5.07989 10 6.2 10L17.8 10Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
<path
d="M17.8 21C18.9201 21 19.4802 21 19.908 20.782C20.2843 20.5903 20.5903 20.2843 20.782 19.908C21 19.4802 21 18.9201 21 17.8V17.2C21 16.0799 21 15.5198 20.782 15.092C20.5903 14.7157 20.2843 14.4097 19.908 14.218C19.4802 14 18.9201 14 17.8 14L6.2 14C5.0799 14 4.51984 14 4.09202 14.218C3.71569 14.4097 3.40973 14.7157 3.21799 15.092C3 15.5198 3 16.0799 3 17.2L3 17.8C3 18.9201 3 19.4802 3.21799 19.908C3.40973 20.2843 3.71569 20.5903 4.09202 20.782C4.51984 21 5.07989 21 6.2 21H17.8Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
<p class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950">
Unlimited fields
</p>
</div>
<div class="flex flex-col items-center gap-3 md:flex-row">
<svg aria-hidden="true" class="w-6 h-6 shrink-0 stroke-blue-600" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M9 11L12 14L22 4M16 3H7.8C6.11984 3 5.27976 3 4.63803 3.32698C4.07354 3.6146 3.6146 4.07354 3.32698 4.63803C3 5.27976 3 6.11984 3 7.8V16.2C3 17.8802 3 18.7202 3.32698 19.362C3.6146 19.9265 4.07354 20.3854 4.63803 20.673C5.27976 21 6.11984 21 7.8 21H16.2C17.8802 21 18.7202 21 19.362 20.673C19.9265 20.3854 20.3854 19.9265 20.673 19.362C21 18.7202 21 17.8802 21 16.2V12"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
<p class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950">
Multiple input types
</p>
</div>
<div class="flex flex-col items-center gap-3 md:flex-row">
<svg aria-hidden="true" class="w-6 h-6 shrink-0 stroke-blue-600" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M22 11V8.2C22 7.0799 22 6.51984 21.782 6.09202C21.5903 5.71569 21.2843 5.40973 20.908 5.21799C20.4802 5 19.9201 5 18.8 5H5.2C4.0799 5 3.51984 5 3.09202 5.21799C2.71569 5.40973 2.40973 5.71569 2.21799 6.09202C2 6.51984 2 7.0799 2 8.2V11.8C2 12.9201 2 13.4802 2.21799 13.908C2.40973 14.2843 2.71569 14.5903 3.09202 14.782C3.51984 15 4.0799 15 5.2 15H11M12 10H12.005M17 10H17.005M7 10H7.005M19.25 17V15.25C19.25 14.2835 18.4665 13.5 17.5 13.5C16.5335 13.5 15.75 14.2835 15.75 15.25V17M12.25 10C12.25 10.1381 12.1381 10.25 12 10.25C11.8619 10.25 11.75 10.1381 11.75 10C11.75 9.86193 11.8619 9.75 12 9.75C12.1381 9.75 12.25 9.86193 12.25 10ZM17.25 10C17.25 10.1381 17.1381 10.25 17 10.25C16.8619 10.25 16.75 10.1381 16.75 10C16.75 9.86193 16.8619 9.75 17 9.75C17.1381 9.75 17.25 9.86193 17.25 10ZM7.25 10C7.25 10.1381 7.13807 10.25 7 10.25C6.86193 10.25 6.75 10.1381 6.75 10C6.75 9.86193 6.86193 9.75 7 9.75C7.13807 9.75 7.25 9.86193 7.25 10ZM15.6 21H19.4C19.9601 21 20.2401 21 20.454 20.891C20.6422 20.7951 20.7951 20.6422 20.891 20.454C21 20.2401 21 19.9601 21 19.4V18.6C21 18.0399 21 17.7599 20.891 17.546C20.7951 17.3578 20.6422 17.2049 20.454 17.109C20.2401 17 19.9601 17 19.4 17H15.6C15.0399 17 14.7599 17 14.546 17.109C14.3578 17.2049 14.2049 17.3578 14.109 17.546C14 17.7599 14 18.0399 14 18.6V19.4C14 19.9601 14 20.2401 14.109 20.454C14.2049 20.6422 14.3578 20.7951 14.546 20.891C14.7599 21 15.0399 21 15.6 21Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
<p class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950">
Form password
</p>
</div>
<div class="flex flex-col items-center gap-3 md:flex-row">
<svg aria-hidden="true" class="w-6 h-6 shrink-0 stroke-blue-600" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M18 15C16.3431 15 15 16.3431 15 18C15 19.6569 16.3431 21 18 21C19.6569 21 21 19.6569 21 18C21 16.3431 19.6569 15 18 15ZM18 15V8C18 7.46957 17.7893 6.96086 17.4142 6.58579C17.0391 6.21071 16.5304 6 16 6H13M6 9C7.65685 9 9 7.65685 9 6C9 4.34315 7.65685 3 6 3C4.34315 3 3 4.34315 3 6C3 7.65685 4.34315 9 6 9ZM6 9V21"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
<p class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950">
Webhooks
</p>
</div>
<div class="flex flex-col items-center gap-3 md:flex-row">
<svg aria-hidden="true" class="w-6 h-6 shrink-0 stroke-blue-600" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M21 8H3M16 2V5M8 2V5M12 18V12M9 15H15M7.8 22H16.2C17.8802 22 18.7202 22 19.362 21.673C19.9265 21.3854 20.3854 20.9265 20.673 20.362C21 19.7202 21 18.8802 21 17.2V8.8C21 7.11984 21 6.27976 20.673 5.63803C20.3854 5.07354 19.9265 4.6146 19.362 4.32698C18.7202 4 17.8802 4 16.2 4H7.8C6.11984 4 5.27976 4 4.63803 4.32698C4.07354 4.6146 3.6146 5.07354 3.32698 5.63803C3 6.27976 3 7.11984 3 8.8V17.2C3 18.8802 3 19.7202 3.32698 20.362C3.6146 20.9265 4.07354 21.3854 4.63803 21.673C5.27976 22 6.11984 22 7.8 22Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
<p class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950">
Closing date
</p>
</div>
<div class="flex flex-col items-center col-span-2 gap-3 md:flex-row sm:col-span-1">
<svg aria-hidden="true" class="w-6 h-6 shrink-0 stroke-blue-600" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M12 6C12.5523 6 13 5.55228 13 5C13 4.44772 12.5523 4 12 4C11.4477 4 11 4.44772 11 5C11 5.55228 11.4477 6 12 6Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
<path
d="M12 13C12.5523 13 13 12.5523 13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
<path
d="M12 20C12.5523 20 13 19.5523 13 19C13 18.4477 12.5523 18 12 18C11.4477 18 11 18.4477 11 19C11 19.5523 11.4477 20 12 20Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
<path
d="M19 6C19.5523 6 20 5.55228 20 5C20 4.44772 19.5523 4 19 4C18.4477 4 18 4.44772 18 5C18 5.55228 18.4477 6 19 6Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
<path
d="M19 13C19.5523 13 20 12.5523 20 12C20 11.4477 19.5523 11 19 11C18.4477 11 18 11.4477 18 12C18 12.5523 18.4477 13 19 13Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
<path
d="M19 20C19.5523 20 20 19.5523 20 19C20 18.4477 19.5523 18 19 18C18.4477 18 18 18.4477 18 19C18 19.5523 18.4477 20 19 20Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
<path
d="M5 6C5.55228 6 6 5.55228 6 5C6 4.44772 5.55228 4 5 4C4.44772 4 4 4.44772 4 5C4 5.55228 4.44772 6 5 6Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
<path
d="M5 13C5.55228 13 6 12.5523 6 12C6 11.4477 5.55228 11 5 11C4.44772 11 4 11.4477 4 12C4 12.5523 4.44772 13 5 13Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
<path
d="M5 20C5.55228 20 6 19.5523 6 19C6 18.4477 5.55228 18 5 18C4.44772 18 4 18.4477 4 19C4 19.5523 4.44772 20 5 20Z"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
<p class="text-base font-semibold sm:text-lg lg:text-xl text-gray-950">
And much more...
</p>
</div>
</div>
<div class="max-w-3xl p-6 mx-auto mt-12 sm:mt-16 bg-yellow-50 ring-1 ring-inset ring-yellow-200 rounded-2xl">
<div class="flex items-start gap-4">
<svg aria-hidden="true" class="w-8 h-8 shrink-0 stroke-yellow-500" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M12 21L11.8999 20.8499C11.2053 19.808 10.858 19.287 10.3991 18.9098C9.99286 18.5759 9.52476 18.3254 9.02161 18.1726C8.45325 18 7.82711 18 6.57482 18H5.2C4.07989 18 3.51984 18 3.09202 17.782C2.71569 17.5903 2.40973 17.2843 2.21799 16.908C2 16.4802 2 15.9201 2 14.8V6.2C2 5.07989 2 4.51984 2.21799 4.09202C2.40973 3.71569 2.71569 3.40973 3.09202 3.21799C3.51984 3 4.07989 3 5.2 3H5.6C7.84021 3 8.96031 3 9.81596 3.43597C10.5686 3.81947 11.1805 4.43139 11.564 5.18404C12 6.03968 12 7.15979 12 9.4M12 21V9.4M12 21L12.1001 20.8499C12.7947 19.808 13.142 19.287 13.6009 18.9098C14.0071 18.5759 14.4752 18.3254 14.9784 18.1726C15.5467 18 16.1729 18 17.4252 18H18.8C19.9201 18 20.4802 18 20.908 17.782C21.2843 17.5903 21.5903 17.2843 21.782 16.908C22 16.4802 22 15.9201 22 14.8V6.2C22 5.07989 22 4.51984 21.782 4.09202C21.5903 3.71569 21.2843 3.40973 20.908 3.21799C20.4802 3 19.9201 3 18.8 3H18.4C16.1598 3 15.0397 3 14.184 3.43597C13.4314 3.81947 12.8195 4.43139 12.436 5.18404C12 6.03968 12 7.15979 12 9.4"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
<div>
<p class="text-lg font-semibold text-yellow-600">
Nonprofit & Student Discount 50%
</p>
<p class="mt-1 text-base font-medium leading-7 text-yellow-600">
Whether your nonprofit is large or small, OpnForms online Form Builder helps your organization help
others. It takes just a few minutes to create and publish your forms online. As an exclusive benefit,
we offer nonprofits & students a 50-percent discount!
</p>
</div>
</div>
</div>
</div>
</section>
<section class="py-12 bg-gray-50 border-t border-gray-200 sm:py-16 lg:py-20 xl:py-24">
<div class="px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl">
<div class="max-w-3xl mx-auto text-center">
<h2 class="text-3xl font-semibold tracking-tight text-gray-900 sm:text-4xl lg:leading-tight">
Got any question?
</h2>
<p class="max-w-2xl mx-auto mt-4 text-base font-medium leading-7 text-gray-600 sm:text-lg sm:leading-8">
We've compiled a list of the most common questions we get asked.
</p>
</div>
<dl
class="grid max-w-5xl grid-cols-1 mx-auto mt-12 gap-y-12 sm:grid-cols-2 sm:mt-16 sm:gap-x-8 sm:gap-y-12 lg:gap-x-10">
<div v-for="q in [
{
'question':'Is there a free version of OpnForm available?',
'answer':'Yes, OpnForm offers a free version with access to 99% of features, including unlimited forms, submissions, fields, and more. Our goal is to provide robust functionality for all users without limitations.',
},
{
'question':'What does the Pro Plan include?',
'answer':'The OpnForm Pro Plan is designed to meet the advanced needs of teams and creators. It includes features like form confirmation emails, Slack and Discord notifications, editable submissions, custom domain, custom code integration, larger file uploads, removal of OpnForm branding, priority support, and more.',
},
{
'question':'Can I try the Pro Plan before subscribing?',
'answer':'Absolutely! We offer a free 3-day trial of the OpnForm Pro Plan. This allows you to explore all the empowering features and experience the value it brings to your form-building process. The trial is automatically applied.',
},
{
'question':'Is there a discount for annual plans?',
'answer':'Yes, we offer a 20% discount for annual Pro Plan subscriptions. By choosing the yearly billing option, you can enjoy the same great features at a reduced cost.',
},
{
'question':'How does the nonprofit and student discount work?',
'answer':'OpnForm is committed to supporting nonprofits and students. We provide an exclusive 50% discount on the Pro Plan for nonprofit organizations and students. This discount helps you make the most of our form builder while staying within your budget.',
},
{
'question':'Can I cancel or change my plan at any time?',
'answer':'Yes, you have the flexibility to upgrade, downgrade, or cancel your OpnForm Pro Plan at any time. Changes will take effect immediately, and you\'ll only be billed based on the plan you\'re currently on.',
},
]" :key="q.question">
<dt class="text-base font-semibold leading-7 text-gray-950 sm:text-lg sm:leading-8">
{{ q.question }}
</dt>
<dd class="mt-2 text-base font-medium leading-7 text-gray-600">
{{ q.answer }}
</dd>
</div>
</dl>
<div class="mt-12 text-center sm:mt-16">
<p class="text-base font-medium text-gray-950">
Didn't find the answer? <a href="#" @click.prevent="contactUs"
class="font-semibold text-blue-600 hover:underline">Contact us</a>
</p>
</div>
</div>
</section>
<open-form-footer/>
</div>
</template>
<script>
import { computed } from 'vue'
import { useAuthStore } from '../stores/auth';
import OpenFormFooter from '../components/pages/OpenFormFooter.vue'
import PricingTable from '../components/pages/pricing/PricingTable.vue'
import SeoMeta from '../mixins/seo-meta.js'
export default {
components: {OpenFormFooter, PricingTable},
mixins: [SeoMeta],
layout: 'default',
props: {},
beforeRouteEnter(to, from, next) {
if (!this.$config.paid_plans_enabled) { // If no paid plan so no need this page
next({name: 'home'})
return
}
next()
},
setup () {
const authStore = useAuthStore()
return {
user : computed(() => authStore.user),
authenticated : computed(() => authStore.check)
}
},
data: () => ({
metaTitle: 'Pricing',
metaDescription: 'All of our core features are free, and there is no quantity limit. You can also created more advanced and customized forms with OpnForms Pro.',
}),
mounted() {},
computed: {},
methods: {
contactUs() {
window.$crisp.push(['do', 'chat:show'])
window.$crisp.push(['do', 'chat:open'])
}
}
}
</script>

View File

@@ -0,0 +1,33 @@
<template>
<div>
<div class="mt-6 flex flex-col">
<div class="w-full md:max-w-3xl md:mx-auto px-4 md:pt-16">
<h1 class="sm:text-5xl">
Privacy Policy
</h1>
<NotionPage page-id="9c97349ceda7455aab9b341d1ff70f79" />
</div>
</div>
<open-form-footer />
</div>
</template>
<script>
import OpenFormFooter from '../components/pages/OpenFormFooter.vue'
import SeoMeta from '../mixins/seo-meta.js'
export default {
components: { OpenFormFooter},
layout: 'default',
mixins: [SeoMeta],
data: () => ({
metaTitle: 'Privacy Policy',
}),
computed: {},
mounted () {
}
}
</script>

84
client/pages/register.vue Normal file
View File

@@ -0,0 +1,84 @@
<template>
<div>
<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>
<p class="text-gray-900 my-4 text-lg">
It takes seconds, you don't need to know how to code and it's free.
</p>
<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"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
Unlimited forms
</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"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
Unlimited fields
</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"
>
<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>
</div>
</div>
<open-form-footer />
</div>
</template>
<script>
import OpenFormFooter from '../../components/pages/OpenFormFooter.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: {
AppSumoRegister,
OpenFormFooter,
RegisterForm
},
mixins: [SeoMeta],
middleware: 'guest',
data: () => ({
metaTitle: 'Register'
}),
computed: {},
methods: {}
}
</script>

View File

@@ -0,0 +1,61 @@
<template>
<div>
<h3 class="font-semibold text-2xl text-gray-900">Danger zone</h3>
<p class="text-gray-600 text-sm mt-2">
This will permanently delete your entire account. All your forms, submissions and workspaces will be deleted.
<span class="text-red-500">
This cannot be undone.
</span>
</p>
<!-- Submit Button -->
<v-button :loading="loading" class="mt-4" color="red" @click="alertConfirm('Do you really want to delete your account?',deleteAccount)">
Delete account
</v-button>
</div>
</template>
<script>
import Form from 'vform'
import axios from 'axios'
import { useAuthStore } from '../../stores/auth'
import SeoMeta from '../../mixins/seo-meta.js'
export default {
scrollToTop: false,
mixins: [SeoMeta],
setup () {
const authStore = useAuthStore()
return {
authStore
}
},
data: () => ({
metaTitle: 'Account',
form: new Form({
identifier: ''
}),
loading: false
}),
methods: {
async deleteAccount () {
this.loading = true
axios.delete('/api/user').then(async (response) => {
this.loading = false
this.alertSuccess(response.data.message)
// Log out the user.
await this.authStore.logout()
// Redirect to login.
this.$router.push({ name: 'login' })
}).catch((error) => {
this.alertError(error.response.data.message)
this.loading = false
})
}
}
}
</script>

View File

@@ -0,0 +1,92 @@
<template>
<div>
<h3 class="font-semibold text-2xl text-gray-900">Admin settings</h3>
<small class="text-gray-600">Manage settings.</small>
<h3 class="mt-3 text-lg font-semibold mb-4">
Tools
</h3>
<div class="flex flex-wrap mb-5">
<a href="/stats">
<v-button class="mx-1" color="gray" shade="lighter">
Stats
</v-button>
</a>
<a href="/horizon">
<v-button class="mx-1" color="gray" shade="lighter">
Horizon
</v-button>
</a>
</div>
<h3 class="text-lg font-semibold mb-4">
Impersonate User
</h3>
<form @submit.prevent="impersonate" @keydown="form.onKeydown($event)">
<!-- Password -->
<text-input name="identifier" :form="form" label="Identifier"
:required="true" help="User Id, User Email or Form Slug"
/>
<!-- Submit Button -->
<v-button :loading="loading" class="mt-4">Impersonate User</v-button>
</form>
</div>
</template>
<script>
import Form from 'vform'
import axios from 'axios'
import { useAuthStore } from '../../stores/auth'
import { useWorkspacesStore } from '../../stores/workspaces'
import SeoMeta from '../../mixins/seo-meta.js'
export default {
components: { },
middleware: 'admin',
scrollToTop: false,
mixins: [SeoMeta],
setup () {
const authStore = useAuthStore()
const workspacesStore = useWorkspacesStore()
return {
authStore,
workspacesStore
}
},
data: () => ({
metaTitle: 'Admin',
form: new Form({
identifier: ''
}),
loading: false
}),
methods: {
async impersonate () {
this.loading = true
this.authStore.startImpersonating()
axios.get('/api/admin/impersonate/' + encodeURI(this.form.identifier)).then(async (response) => {
this.loading = false
// Save the token.
this.authStore.saveToken(response.data.token, false)
// Fetch the user.
await this.authStore.fetchUser()
// Redirect to the dashboard.
this.workspacesStore.set([])
this.$router.push({ name: 'home' })
}).catch((error) => {
this.alertError(error.response.data.message)
this.loading = false
})
// this.form.reset()
}
}
}
</script>

View File

@@ -0,0 +1,63 @@
<template>
<div>
<h3 class="font-semibold text-2xl text-gray-900">
Billing details
</h3>
<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>
<script>
import axios from 'axios'
import { computed } from 'vue'
import { useAuthStore } from '../../stores/auth'
import VButton from '~/components/global/VButton.vue'
import SeoMeta from '../../mixins/seo-meta.js'
import AppSumoBilling from '../../components/vendor/appsumo/AppSumoBilling.vue'
export default {
components: { AppSumoBilling, VButton },
mixins: [SeoMeta],
scrollToTop: false,
setup () {
const authStore = useAuthStore()
return {
user : computed(() => authStore.user)
}
},
data: () => ({
metaTitle: 'Billing',
billingLoading: false
}),
methods: {
openBillingDashboard () {
this.billingLoading = true
axios.get('/api/subscription/billing-portal').then((response) => {
const url = response.data.portal_url
window.location = url
}).catch((error) => {
this.alertError(error.response.data.message)
}).finally(() => {
this.billingLoading = false
})
}
},
computed: {}
}
</script>

View File

@@ -0,0 +1,105 @@
<template>
<div class="bg-white">
<div class="flex bg-gray-50">
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4">
<div class="pt-4 pb-0">
<div class="flex">
<h2 class="flex-grow text-gray-900">
My Account
</h2>
</div>
<ul class="flex text-gray-500">
<li>{{ user.email }}</li>
</ul>
<div class="mt-4 border-b border-gray-200 dark:border-gray-700">
<ul class="flex flex-wrap -mb-px text-sm font-medium text-center">
<li v-for="(tab, i) in tabsList" :key="i+1" class="mr-6">
<router-link :to="{ name: tab.route }"
class="hover:no-underline inline-block py-4 rounded-t-lg border-b-2 text-gray-500 hover:text-gray-600"
active-class="text-blue-600 hover:text-blue-900 dark:text-blue-500 dark:hover:text-blue-500 border-blue-600 dark:border-blue-500"
>
{{ tab.name }}
</router-link>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="flex bg-white">
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4">
<div class="mt-8 pb-0">
<router-view v-slot="{ Component }">
<transition name="page" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</div>
</div>
</div>
</div>
</template>
<script>
import { computed } from 'vue'
import { useAuthStore } from '../../stores/auth'
export default {
middleware: 'auth',
setup () {
const authStore = useAuthStore()
return {
user: computed(() => authStore.user)
}
},
data () {
return {
}
},
computed: {
tabsList () {
const tabs = [
{
name: 'Profile',
route: 'settings.profile'
},
{
name: 'Workspace Settings',
route: 'settings.workspaces'
},
{
name: 'Password',
route: 'settings.password'
},
{
name: 'Delete Account',
route: 'settings.account'
}
]
if (this.user.is_subscribed) {
tabs.splice(1, 0, {
name: 'Billing',
route: 'settings.billing'
})
}
if (this.user.admin) {
tabs.push({
name: 'Admin',
route: 'settings.admin'
})
}
return tabs
}
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,53 @@
<template>
<div>
<h3 class="font-semibold text-2xl text-gray-900">
Password
</h3>
<small class="text-gray-600">Manage your password.</small>
<form class="mt-3" @submit.prevent="update" @keydown="form.onKeydown($event)">
<alert-success class="mb-5" :form="form" message="Password updated." />
<!-- Password -->
<text-input native-type="password"
name="password" :form="form" label="Password" :required="true"
/>
<!-- Password Confirmation-->
<text-input native-type="password"
name="password_confirmation" :form="form" label="Confirm Password" :required="true"
/>
<!-- Submit Button -->
<v-button :loading="form.busy" class="mt-4">
Update password
</v-button>
</form>
</div>
</template>
<script>
import Form from 'vform'
import SeoMeta from '../../mixins/seo-meta.js'
export default {
mixins: [SeoMeta],
scrollToTop: false,
data: () => ({
metaTitle: 'Password',
form: new Form({
password: '',
password_confirmation: ''
})
}),
methods: {
async update () {
await this.form.patch('/api/settings/password')
this.form.reset()
}
}
}
</script>

View File

@@ -0,0 +1,66 @@
<template>
<div>
<h3 class="font-semibold text-2xl text-gray-900">
Profile details
</h3>
<small class="text-gray-600">Update your username and manage your account details.</small>
<form class="mt-3" @submit.prevent="update" @keydown="form.onKeydown($event)">
<alert-success class="mb-5" :form="form" message="Your info has been updated!" />
<!-- Name -->
<text-input name="name" :form="form" label="Name" :required="true" />
<!-- Email -->
<text-input name="email" :form="form" label="Email" :required="true" />
<!-- Submit Button -->
<v-button :loading="form.busy" class="mt-4">
Save changes
</v-button>
</form>
</div>
</template>
<script>
import Form from 'vform'
import { computed } from 'vue'
import { useAuthStore } from '../../stores/auth'
import SeoMeta from '../../mixins/seo-meta.js'
export default {
mixins: [SeoMeta],
scrollToTop: false,
setup () {
const authStore = useAuthStore()
return {
authStore,
user : computed(() => authStore.user)
}
},
data: () => ({
metaTitle: 'Profile',
form: new Form({
name: '',
email: ''
})
}),
created () {
// Fill the form with user data.
this.form.keys().forEach(key => {
this.form[key] = this.user[key]
})
},
methods: {
async update () {
const { data } = await this.form.patch('/api/settings/profile')
this.authStore.updateUser(data)
}
}
}
</script>

View File

@@ -0,0 +1,224 @@
<template>
<div>
<div class="flex flex-wrap items-center gap-y-4 flex-wrap-reverse">
<div class="flex-grow">
<h3 class="font-semibold text-2xl text-gray-900">
Workspace settings
</h3>
<small class="text-gray-600">Manage your workspaces.</small>
</div>
<v-button color="outline-blue" :loading="loading" @click="workspaceModal=true">
<svg class="inline -mt-1 mr-1 h-4 w-4" viewBox="0 0 14 14" fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M6.99996 1.16699V12.8337M1.16663 7.00033H12.8333" stroke="currentColor" stroke-width="1.67"
stroke-linecap="round" stroke-linejoin="round"
/>
</svg>
Create new workspace
</v-button>
</div>
<div v-if="loading" class="w-full text-blue-500 text-center">
<loader class="h-10 w-10 p-5" />
</div>
<div v-else-if="workspace">
<div class="mt-4 flex group bg-white items-center">
<div class="flex space-x-4 flex-grow items-center">
<img v-if="isUrl(workspace.icon)" :src="workspace.icon" :alt="workspace.name + ' icon'"
class="rounded-full h-12 w-12"
>
<div v-else class="rounded-2xl bg-gray-100 h-12 w-12 text-2xl pt-2 text-center overflow-hidden"
v-text="workspace.icon"
/>
<div class="space-y-4 py-1">
<div class="font-bold truncate">
{{ workspace.name }}
</div>
</div>
</div>
</div>
<template v-if="customDomainsEnabled">
<text-area-input v-model="customDomains" name="custom_domain" class="mt-4" :required="false"
:disabled="!workspace.is_pro"
label="Workspace Custom Domains" wrapper-class="" placeholder="yourdomain.com - 1 per line"
/>
<p class="text-gray-500 text-sm">
Read our <a href="#"
@click.prevent="$crisp.push(['do', 'helpdesk:article:open', ['en', 'how-to-use-my-own-domain-9m77g7']])"
>custom
domain instructions</a> to learn how to use your own domain.
</p>
</template>
<div class="flex flex-wrap justify-between gap-2 mt-4">
<v-button v-if="customDomainsEnabled" class="w-full sm:w-auto" :loading="customDomainsLoading" @click="saveChanges">
<svg class="w-4 h-4 text-white inline mr-1 -mt-1" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17 21V13H7V21M7 3V8H15M19 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H16L21 8V19C21 19.5304 20.7893 20.0391 20.4142 20.4142C20.0391 20.7893 19.5304 21 19 21Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
/>
</svg>
Save Domains
</v-button>
<v-button v-if="workspaces.length > 1" color="white" class="group w-full sm:w-auto" :loading="loading"
@click="deleteWorkspace(workspace)"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 -mt-1 inline group-hover:text-red-700" fill="none"
viewBox="0 0 24 24" stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
Remove workspace
</v-button>
</div>
</div>
<!-- Workspace modal -->
<modal :show="workspaceModal" max-width="lg" @close="workspaceModal=false">
<template #icon>
<svg class="w-8 h-8" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12 8V16M8 12H16M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
/>
</svg>
</template>
<template #title>
Create Workspace
</template>
<div class="px-4">
<form @submit.prevent="createWorkspace" @keydown="form.onKeydown($event)">
<div>
<text-input name="name" class="mt-4" :form="form" :required="true"
label="Workspace Name"
/>
<text-input name="emoji" class="mt-4" :form="form" :required="false"
label="Emoji"
/>
</div>
<div class="w-full mt-6">
<v-button :loading="form.busy" class="w-full my-3">
Save
</v-button>
</div>
</form>
</div>
</modal>
</div>
</template>
<script>
import { computed } from 'vue'
import Form from 'vform'
import { useFormsStore } from '../../stores/forms'
import { useWorkspacesStore } from '../../stores/workspaces'
import SeoMeta from '../../mixins/seo-meta.js'
import TextAreaInput from '../../components/forms/TextAreaInput.vue'
import axios from 'axios'
import * as domain from 'domain'
export default {
components: { TextAreaInput },
mixins: [SeoMeta],
scrollToTop: false,
mixins: [SeoMeta],
setup () {
const formsStore = useFormsStore()
const workspacesStore = useWorkspacesStore()
return {
formsStore,
workspacesStore,
workspaces: computed(() => workspacesStore.content),
loading: computed(() => workspacesStore.loading)
}
},
data: () => ({
metaTitle: 'Workspaces',
form: new Form({
name: '',
emoji: ''
}),
workspaceModal: false,
customDomains: '',
customDomainsLoading: false
}),
mounted () {
this.workspacesStore.loadIfEmpty()
this.initCustomDomains()
},
computed: {
workspace () {
return this.workspacesStore.getCurrent()
},
customDomainsEnabled () {
return this.$config.custom_domains_enabled
}
},
methods: {
saveChanges () {
if (this.customDomainsLoading) return
this.customDomainsLoading = true
// Update the workspace custom domain
axios.put('/api/open/workspaces/' + this.workspace.id + '/custom-domains', {
custom_domains: this.customDomains.split('\n')
.map(domain => domain.trim())
.filter(domain => domain && domain.length > 0)
}).then((response) => {
this.workspacesStore.addOrUpdate(response.data)
this.alertSuccess('Custom domains saved.')
}).catch((error) => {
this.alertError('Failed to update custom domains: ' + error.response.data.message)
}).finally(() => {
this.customDomainsLoading = false
})
},
initCustomDomains () {
if (!this.workspace) return
this.customDomains = this.workspace.custom_domains.join('\n')
},
deleteWorkspace (workspace) {
if (this.workspaces.length <= 1) {
this.alertError('You cannot delete your only workspace.')
return
}
this.alertConfirm('Do you really want to delete this workspace? All forms created in this workspace will be removed.', () => {
this.workspacesStore.delete(workspace.id).then(() => {
this.alertSuccess('Workspace successfully removed.')
})
})
},
isUrl (str) {
const pattern = new RegExp('^(https?:\\/\\/)?' + // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
'(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
'(\\#[-a-z\\d_]*)?$', 'i') // fragment locator
return !!pattern.test(str)
},
async createWorkspace() {
const {data} = await this.form.post('/api/open/workspaces/create')
this.workspacesStore.load()
this.workspaceModal = false
}
},
watch: {
workspace () {
this.initCustomDomains()
}
}
}
</script>

View File

@@ -0,0 +1,32 @@
<template />
<script>
import { computed } from 'vue'
import { useAuthStore } from '../../stores/auth'
import SeoMeta from '../../mixins/seo-meta.js'
export default {
components: { },
layout: 'default',
middleware: 'auth',
mixins: [SeoMeta],
setup () {
const authStore = useAuthStore()
return {
authenticated : computed(() => authStore.check),
}
},
data: () => ({
metaTitle: 'Error',
}),
mounted () {
this.$router.push({ name: 'pricing' })
this.alertError('Unfortunately we could not confirm your subscription. Please try again and contact us if the issue persists.')
},
computed: {}
}
</script>

View File

@@ -0,0 +1,78 @@
<template>
<div class="flex flex-col min-h-screen">
<div class="w-full md:max-w-3xl md:mx-auto px-4 mb-10 md:pb-20 md:pt-16 text-center flex-grow">
<h1 class="text-4xl font-semibold">
Thank you!
</h1>
<h4 class="text-xl mt-6">
We're checking the status of your subscription please wait a moment...
</h4>
<div class="text-center">
<loader class="h-6 w-6 text-nt-blue mx-auto mt-20" />
</div>
</div>
<open-form-footer />
</div>
</template>
<script>
import { computed } from 'vue'
import { useAuthStore } from '../../stores/auth'
import OpenFormFooter from '../../components/pages/OpenFormFooter.vue'
import SeoMeta from '../../mixins/seo-meta.js'
export default {
components: { OpenFormFooter },
mixins: [SeoMeta],
layout: 'default',
middleware: 'auth',
setup () {
const authStore = useAuthStore()
return {
authStore,
authenticated : computed(() => authStore.check),
user : computed(() => authStore.user)
}
},
data: () => ({
metaTitle: 'Subscription Success',
interval: null
}),
mounted () {
this.redirectIfSubscribed()
this.interval = setInterval(() => this.checkSubscription(), 5000)
},
beforeUnmount () {
clearInterval(this.interval)
},
methods: {
async checkSubscription () {
// Fetch the user.
await this.authStore.fetchUser()
this.redirectIfSubscribed()
},
redirectIfSubscribed () {
if (this.user.is_subscribed) {
this.$logEvent('subscribed', { plan: this.user.has_enterprise_subscription ? 'enterprise' : 'pro' })
this.$crisp.push(['set', 'session:event', [[['subscribed', { plan: this.user.has_enterprise_subscription ? 'enterprise' : 'pro' }, 'blue']]]])
this.$router.push({ name: 'home' })
if (this.user.has_enterprise_subscription) {
this.alertSuccess('Awesome! Your subscription to OpnForm is now confirmed! You now have access to all Enterprise ' +
'features. No need to invite your teammates, just ask them to create a OpnForm account and to connect the same Notion workspace. Feel free to contact us if you have any question 🙌')
} else {
this.alertSuccess('Awesome! Your subscription to OpnForm is now confirmed! You now have access to all Pro ' +
'features. Feel free to contact us if you have any question 🙌')
}
}
}
},
computed: {}
}
</script>

View File

@@ -0,0 +1,47 @@
<template>
<div class="flex flex-col min-h-full border-t">
<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 max-w-xl mx-auto">
<h1 class="text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight text-gray-900">
Form Templates
</h1>
<p class="text-gray-600 mt-4 text-lg font-normal">
Our collection of beautiful templates to create your own forms!
</p>
</div>
</div>
</section>
<templates-list />
<open-form-footer class="mt-8 border-t"/>
</div>
</template>
<script>
import OpenFormFooter from '../components/pages/OpenFormFooter.vue'
import TemplatesList from '../components/pages/templates/TemplatesList.vue'
import SeoMeta from '../mixins/seo-meta.js'
export default {
components: { OpenFormFooter, TemplatesList },
mixins: [SeoMeta],
props: {
metaTitle: { type: String, default: 'Templates' },
metaDescription: { type: String, default: 'Our collection of beautiful templates to create your own forms!' }
},
data () {
return {}
},
mounted () {},
computed: {},
methods: {}
}
</script>

View File

@@ -0,0 +1,238 @@
<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 Form from 'vform'
import Fuse from 'fuse.js'
import { computed } from 'vue'
import { useAuthStore } from '../../stores/auth'
import { useTemplatesStore } from '../../stores/templates'
import SeoMeta from '../../mixins/seo-meta.js'
import OpenFormFooter from '../../components/pages/OpenFormFooter.vue'
import Breadcrumb from '~/components/global/Breadcrumb.vue'
import SingleTemplate from '../../components/pages/templates/SingleTemplate.vue'
const loadTemplates = function () {
const templatesStore = useTemplatesStore()
templatesStore.startLoading()
templatesStore.loadIfEmpty().then(() => {
templatesStore.stopLoading()
})
}
export default {
components: { Breadcrumb, OpenFormFooter, SingleTemplate },
mixins: [SeoMeta],
beforeRouteEnter (to, from, next) {
loadTemplates()
next()
},
setup () {
const authStore = useAuthStore()
const templatesStore = useTemplatesStore()
return {
authStore,
authenticated : computed(() => authStore.check),
user : computed(() => authStore.user),
templates : computed(() => templatesStore.content),
templatesLoading : computed(() => templatesStore.loading),
industries : computed(() => templatesStore.industries),
types : computed(() => templatesStore.types)
}
},
data () {
return {
selectedType: 'all',
searchTemplate: new Form({
search: ''
})
}
},
mounted () {},
computed: {
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,44 @@
<template>
<div class="flex flex-col min-h-full border-t">
<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 max-w-xl mx-auto">
<h1 class="text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight text-gray-900">
My Form Templates
</h1>
<p class="text-gray-600 mt-4 text-lg font-normal">
Share your best form as templates so that others can re-use them!
</p>
</div>
</div>
</section>
<templates-list :only-my="true" />
</div>
</template>
<script>
import TemplatesList from '../../components/pages/templates/TemplatesList.vue'
import SeoMeta from '../../mixins/seo-meta.js'
export default {
components: { TemplatesList },
mixins: [SeoMeta],
middleware: 'auth',
props: {
metaTitle: { type: String, default: 'My Templates' },
metaDescription: { type: String, default: 'Our collection of beautiful templates to create your own forms!' }
},
data () {
return {}
},
mounted () {},
computed: {},
methods: {}
}
</script>

View File

@@ -0,0 +1,328 @@
<template>
<div class="flex flex-col min-h-full">
<breadcrumb :path="breadcrumbs">
<template #left>
<div v-if="canEditTemplate" class="ml-5">
<v-button color="gray" size="small" @click.prevent="showFormTemplateModal=true">
Edit Template
</v-button>
<form-template-modal v-if="form" :form="form" :template="template" :show="showFormTemplateModal"
@close="showFormTemplateModal=false"
/>
</div>
</template>
<template #right>
<v-button v-if="canEditTemplate" v-track.copy_template_button_clicked size="small" color="white" class="mr-5"
@click.prevent="copyTemplateUrl"
>
Copy Template URL
</v-button>
<v-button v-track.use_template_button_clicked size="small" class="mr-5"
:to="{path: createFormWithTemplateUrl}"
>
Use this template
</v-button>
</template>
</breadcrumb>
<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="template === null || !template" class="text-center my-4">
We could not find this template.
</p>
<template v-else>
<section class="pt-12 bg-gray-50 sm:pt-16 border-b pb-[250px] relative">
<div class="px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl">
<div class="flex flex-col items-center justify-center max-w-4xl gap-8 mx-auto md:gap-12 md:flex-row">
<div class="aspect-[4/3] shrink-0 rounded-lg shadow-sm overflow-hidden group max-w-xs">
<img class="object-cover w-full h-full transition-all duration-200 group-hover:scale-110"
:src="template.image_url" alt="Template cover image"
>
</div>
<div class="flex-1 text-center md:text-left relative">
<h1 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
{{ template.name }}
</h1>
<p class="mt-2 text-lg font-normal text-gray-600">
{{ cleanQuotes(template.short_description) }}
</p>
<template-tags :slug="template.slug" :display-all="true"
class="flex flex-wrap items-center justify-center gap-3 mt-4 md:justify-start"
/>
</div>
</div>
</div>
</section>
<section class="relative px-4 mx-auto sm:px-6 lg:px-8 -mt-[210px]">
<div class="max-w-7xl">
<div
class="max-w-2xl p-4 mx-auto bg-white shadow-lg sm:p-6 lg:p-8 rounded-xl ring-1 ring-inset ring-gray-200 isolate"
>
<p class="text-sm font-medium text-center text-gray-500 -mt-2 mb-2">
Template Preview
</p>
<open-complete-form ref="open-complete-form" :form="form" :creating="true"
class="mb-4 p-4 bg-gray-50 border border-gray-200 border-dashed rounded-lg"
/>
</div>
</div>
<div class="absolute bottom-0 translate-y-full inset-x-0">
<div class="px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl -mt-[20px]">
<div class="flex items-center justify-center">
<v-button v-track.use_template_button_clicked class="mx-auto w-full max-w-[300px]" :to="{path: createFormWithTemplateUrl}">
Use this template
</v-button>
</div>
<div class="flex items-center justify-center">
<div class="text-left mx-auto text-gray-500 text-xs mt-4">
Core features 100% free<br>
No credit card required<br>
No submissions limit on Free plan
</div>
</div>
</div>
</div>
</section>
<section class="pt-20 pb-12 bg-white sm:pb-16">
<div class="px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl">
<div class="max-w-2xl mx-auto mt-16 space-y-12 sm:mt-16 sm:space-y-16">
<div class="nf-text" v-html="template.description" />
<template v-if="template.questions.length > 0">
<hr class="mt-12 border-gray-200">
<div>
<div class="text-center">
<h3 class="text-xl font-bold tracking-tight text-gray-900 sm:text-2xl">
Frequently asked questions
</h3>
<p class="mt-2 text-base font-normal text-gray-600">
Everything you need to know about this template.
</p>
</div>
<dl class="mt-12 space-y-10">
<div v-for="(ques,ques_key) in template.questions" :key="ques_key" class="space-y-4">
<dt class="font-semibold text-gray-900 dark:text-gray-100">
{{ ques.question }}
</dt>
<dd class="mt-2 leading-6 text-gray-600 dark:text-gray-400" v-html="ques.answer" />
</div>
</dl>
</div>
</template>
</div>
</div>
</section>
<section v-if="template.related_templates.length > 0" 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">
Related templates
</h4>
<v-button :to="{name:'templates'}" color="white" size="small" :arrow="true">
View All
</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 sm:gap-y-12">
<single-template v-for="related in template.related_templates" :key="related" :slug="related" />
</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">
<div class="text-center">
<h4 class="text-xl font-bold tracking-tight text-gray-900 sm:text-2xl">
How OpnForm works
</h4>
</div>
<div class="grid grid-cols-1 mt-12 md:grid-cols-2 gap-x-8 gap-y-12">
<div
class="flex flex-col items-center gap-4 text-center lg:items-start sm:text-left sm:items-start xl:flex-row"
>
<div
class="inline-flex items-center justify-center w-10 h-10 text-base font-bold bg-white rounded-full shadow-sm ring-1 ring-inset ring-gray-200 text-blue-500 shrink-0"
>
1
</div>
<div>
<h5 class="text-base font-bold leading-tight text-gray-900">
Copy the template and change it the way you like
</h5>
<p class="mt-2 text-sm font-normal text-gray-600">
<router-link :to="{path:createFormWithTemplateUrl}">
Click here to copy this template
</router-link>
and start customizing it. Change the questions, add new ones, choose colors and
more.
</p>
</div>
</div>
<div
class="flex flex-col items-center gap-4 text-center lg:items-start sm:text-left sm:items-start xl:flex-row"
>
<div
class="inline-flex items-center justify-center w-10 h-10 text-base font-bold bg-white rounded-full shadow-sm ring-1 ring-inset ring-gray-200 text-blue-500 shrink-0"
>
2
</div>
<div>
<h5 class="text-base font-bold leading-tight text-gray-900">
Embed the form or share it via a link
</h5>
<p class="mt-2 text-sm font-normal text-gray-600">
You can directly share your form link, or embed the form on your website. It's magic! 🪄
</p>
</div>
</div>
</div>
<!-- add video here -->
<!-- <div class="max-w-5xl mx-auto mt-12 shadow-sm rounded-xl bg-blue-50 aspect-video" />-->
</div>
</section>
</template>
<open-form-footer class="mt-8 border-t"/>
</div>
</template>
<script>
import Form from 'vform'
import { computed } from 'vue'
import { useAuthStore } from '../../stores/auth'
import { useTemplatesStore } from '../../stores/templates'
import OpenFormFooter from '../../components/pages/OpenFormFooter.vue'
import OpenCompleteForm from '../../components/open/forms/OpenCompleteForm.vue'
import Breadcrumb from '~/components/global/Breadcrumb.vue'
import SeoMeta from '../../mixins/seo-meta.js'
import TemplateTags from '../../components/pages/templates/TemplateTags.vue'
import SingleTemplate from '../../components/pages/templates/SingleTemplate.vue'
import FormTemplateModal from '../../components/open/forms/components/templates/FormTemplateModal.vue'
export default {
components: { Breadcrumb, OpenFormFooter, OpenCompleteForm, TemplateTags, SingleTemplate, FormTemplateModal },
mixins: [SeoMeta],
beforeRouteEnter (to, from, next) {
const templatesStore = useTemplatesStore()
if (to.params?.slug) {
templatesStore.loadTemplate(to.params?.slug)
templatesStore.loadTypesAndIndustries()
}
next()
},
setup () {
const authStore = useAuthStore()
const templatesStore = useTemplatesStore()
return {
templatesStore,
authenticated : computed(() => authStore.check),
user : computed(() => authStore.user),
templatesLoading : computed(() => templatesStore.loading)
}
},
data () {
return {
showFormTemplateModal: false
}
},
mounted () {},
methods: {
cleanQuotes (str) {
// Remove starting and ending quotes if any
return (str) ? str.replace(/^"/, '').replace(/"$/, '') : ''
},
copyTemplateUrl(){
const str = this.template.share_url
const el = document.createElement('textarea')
el.value = str
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
this.alertSuccess('Copied!')
}
},
computed: {
breadcrumbs () {
if (!this.template) {
return [{ route: { name: 'templates' }, label: 'Templates' }]
}
return [{ route: { name: 'templates' }, label: 'Templates' }, { label: this.template.name }]
},
template () {
return this.templatesStore.getBySlug(this.$route.params.slug)
},
form () {
return this.template ? new Form(this.template.structure) : null
},
canEditTemplate () {
return this.user && this.template && (this.user.admin || this.user.template_editor || this.template.creator_id === this.user.id)
},
metaTitle () {
return this.template ? this.template.name : 'Form Template'
},
metaDescription () {
if (!this.template) return null
// take the first 140 characters of the description
return this.template.short_description?.substring(0, 140) + '... | Customize any template and create your own form in minutes.'
},
metaImage () {
if (!this.template) return null
return this.template.image_url
},
metaTags () {
if (!this.template) {
return [];
}
return this.template.publicly_listed ? [] : [{ name: 'robots', content: 'noindex' }]
},
createFormWithTemplateUrl () {
if(this.authenticated) {
return '/forms/create?template=' + this.template?.slug
}
return '/forms/create/guest?template=' + this.template?.slug
}
}
}
</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;
}
}
.aspect-video {
aspect-ratio: 16/9;
}
</style>

View File

@@ -0,0 +1,238 @@
<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 Form from 'vform'
import Fuse from 'fuse.js'
import { computed } from 'vue'
import { useAuthStore } from '../../stores/auth'
import { useTemplatesStore } from '../../stores/templates'
import SeoMeta from '../../mixins/seo-meta.js'
import OpenFormFooter from '../../components/pages/OpenFormFooter.vue'
import Breadcrumb from '~/components/global/Breadcrumb.vue'
import SingleTemplate from '../../components/pages/templates/SingleTemplate.vue'
const loadTemplates = function () {
const templatesStore = useTemplatesStore()
templatesStore.startLoading()
templatesStore.loadIfEmpty().then(() => {
templatesStore.stopLoading()
})
}
export default {
components: { Breadcrumb, OpenFormFooter, SingleTemplate },
mixins: [SeoMeta],
beforeRouteEnter (to, from, next) {
loadTemplates()
next()
},
setup () {
const authStore = useAuthStore()
const templatesStore = useTemplatesStore()
return {
authenticated : computed(() => authStore.check),
user : computed(() => authStore.user),
templates : computed(() => templatesStore.content),
templatesLoading : computed(() => templatesStore.loading),
industries : computed(() => templatesStore.industries),
types : computed(() => templatesStore.types)
}
},
data () {
return {
selectedIndustry: 'all',
searchTemplate: new Form({
search: ''
})
}
},
mounted () {},
computed: {
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>

View File

@@ -0,0 +1,31 @@
<template>
<div>
<div class="mt-6 flex flex-col">
<div class="w-full md:max-w-3xl md:mx-auto px-4 md:pt-16">
<h1 class="sm:text-5xl">
Terms & Conditions
</h1>
<NotionPage page-id="246420da2834480ca04047b0c5a00929" />
</div>
</div>
<open-form-footer />
</div>
</template>
<script>
import OpenFormFooter from '../components/pages/OpenFormFooter.vue'
export default {
components: { OpenFormFooter },
layout: 'default',
data: () => ({
metaTitle: 'Terms & Conditions',
}),
computed: {},
mounted () {
}
}
</script>