Integration pages from Notion (#753)
* Integration pages from Notion * fix integration page * Refactor environment variables and update routing in integration pages - Removed unused environment variables related to MUX from `.env.docker` and `.env.example` to streamline configuration. - Updated routing links in `OpenFormFooter.vue` to correct navigation paths for "Integrations", "Report Abuse", and "Privacy Policy", enhancing user experience. - Added middleware to `definePageMeta` in `index.vue` and `[slug].vue` for integration pages to enforce self-hosted logic. These changes aim to improve code clarity and ensure proper routing functionality across the application. --------- Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
parent
f9c734c826
commit
c17f4776bc
|
|
@ -34,18 +34,7 @@ AWS_SECRET_ACCESS_KEY=
|
||||||
AWS_DEFAULT_REGION=us-east-1
|
AWS_DEFAULT_REGION=us-east-1
|
||||||
AWS_BUCKET=
|
AWS_BUCKET=
|
||||||
|
|
||||||
PUSHER_APP_ID=
|
|
||||||
PUSHER_APP_KEY=
|
|
||||||
PUSHER_APP_SECRET=
|
|
||||||
PUSHER_APP_CLUSTER=mt1
|
|
||||||
|
|
||||||
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
|
||||||
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
|
||||||
|
|
||||||
JWT_TTL=1440
|
JWT_TTL=1440
|
||||||
JWT_SECRET=
|
JWT_SECRET=
|
||||||
|
|
||||||
MUX_WORKSPACE_ID=
|
|
||||||
MUX_API_TOKEN=
|
|
||||||
|
|
||||||
OPEN_AI_API_KEY=
|
OPEN_AI_API_KEY=
|
||||||
|
|
@ -76,9 +76,6 @@ H_CAPTCHA_SECRET_KEY=
|
||||||
RE_CAPTCHA_SITE_KEY=
|
RE_CAPTCHA_SITE_KEY=
|
||||||
RE_CAPTCHA_SECRET_KEY=
|
RE_CAPTCHA_SECRET_KEY=
|
||||||
|
|
||||||
MUX_WORKSPACE_ID=
|
|
||||||
MUX_API_TOKEN=
|
|
||||||
|
|
||||||
ADMIN_EMAILS=
|
ADMIN_EMAILS=
|
||||||
TEMPLATE_EDITOR_EMAILS=
|
TEMPLATE_EDITOR_EMAILS=
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<notion-renderer
|
<NotionRenderer
|
||||||
v-if="!loading"
|
v-if="!loading"
|
||||||
:block-map="blockMap"
|
:block-map="blocks"
|
||||||
|
:block-overrides="blockOverrides"
|
||||||
|
:content-id="contentId"
|
||||||
|
:full-page="fullPage"
|
||||||
|
:hide-list="hideList"
|
||||||
|
:level="level"
|
||||||
|
:map-image-url="mapImageUrl"
|
||||||
|
:map-page-url="mapPageUrl"
|
||||||
|
:page-link-options="pageLinkOptions"
|
||||||
|
:image-options="imageOptions"
|
||||||
|
:prism="prism"
|
||||||
|
:todo="todo"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
|
|
@ -12,27 +23,69 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { NotionRenderer } from "vue-notion"
|
import { NotionRenderer, defaultMapPageUrl, defaultMapImageUrl } from 'vue-notion'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "NotionPage",
|
name: 'NotionPage',
|
||||||
components: { NotionRenderer },
|
components: { NotionRenderer },
|
||||||
props: {
|
props: {
|
||||||
blockMap: {
|
blockMap: {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
blockOverrides: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
},
|
},
|
||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean
|
||||||
required: true,
|
|
||||||
},
|
},
|
||||||
|
contentId: String,
|
||||||
|
contentIndex: { type: Number, default: 0 },
|
||||||
|
fullPage: { type: Boolean, default: false },
|
||||||
|
hideList: { type: Array, default: () => [] },
|
||||||
|
level: { type: Number, default: 0 },
|
||||||
|
mapImageUrl: { type: Function, default: defaultMapImageUrl },
|
||||||
|
mapPageUrl: { type: Function, default: defaultMapPageUrl },
|
||||||
|
pageLinkOptions: {
|
||||||
|
type: Object, default: () => {
|
||||||
|
const NuxtLink = resolveComponent('NuxtLink')
|
||||||
|
return { component: NuxtLink, href: 'to' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
imageOptions: Object,
|
||||||
|
prism: { type: Boolean, default: false },
|
||||||
|
todo: { type: Boolean, default: false }
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
blocks () {
|
||||||
|
if (this.blockMap && this.blockMap.data) {
|
||||||
|
return this.blockMap.data
|
||||||
|
}
|
||||||
|
return this.blockMap
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang='scss'>
|
||||||
@import "vue-notion/src/styles.css";
|
@import "vue-notion/src/styles.css";
|
||||||
|
|
||||||
.notion-blue {
|
.notion-blue {
|
||||||
@apply text-nt-blue;
|
@apply text-nt-blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notion-spacer {
|
||||||
|
width: 24px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notion-link {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notion {
|
||||||
|
img, iframe {
|
||||||
|
@apply rounded-md;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,18 @@
|
||||||
Technical Docs
|
Technical Docs
|
||||||
</a>
|
</a>
|
||||||
<template v-if="!useFeatureFlag('self_hosted')">
|
<template v-if="!useFeatureFlag('self_hosted')">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'integrations' }"
|
||||||
|
class="text-gray-600 dark:text-gray-400 transition-colors duration-300 hover:text-nt-blue"
|
||||||
|
>
|
||||||
|
Integrations
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'report-abuse' }"
|
||||||
|
class="text-gray-600 dark:text-gray-400 transition-colors duration-300 hover:text-nt-blue"
|
||||||
|
>
|
||||||
|
Report Abuse
|
||||||
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'privacy-policy' }"
|
:to="{ name: 'privacy-policy' }"
|
||||||
class="text-gray-600 dark:text-gray-400 transition-colors duration-300 hover:text-nt-blue"
|
class="text-gray-600 dark:text-gray-400 transition-colors duration-300 hover:text-nt-blue"
|
||||||
|
|
@ -63,13 +75,6 @@
|
||||||
>
|
>
|
||||||
Terms & Conditions
|
Terms & Conditions
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'report-abuse' }"
|
|
||||||
class="text-gray-600 dark:text-gray-400 transition-colors duration-300 hover:text-nt-blue"
|
|
||||||
>
|
|
||||||
Report Abuse
|
|
||||||
</router-link>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="innerJson"
|
||||||
|
id="custom-block"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="innerJson.type=='faq'"
|
||||||
|
class="rounded-lg bg-white z-10 pt-10"
|
||||||
|
>
|
||||||
|
<h2 class="font-medium">
|
||||||
|
Frequently Asked Questions
|
||||||
|
</h2>
|
||||||
|
<dl class="pt-4 space-y-6">
|
||||||
|
<div
|
||||||
|
v-for="question in innerJson.content"
|
||||||
|
:key="question.label"
|
||||||
|
class="space-y-2"
|
||||||
|
>
|
||||||
|
<dt class="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{{ question.label }}
|
||||||
|
</dt>
|
||||||
|
<dd
|
||||||
|
class="leading-6 text-gray-600 dark:text-gray-400"
|
||||||
|
v-html="question.content"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="innerJson.type=='cta'"
|
||||||
|
class="rounded-lg relative bg-gradient-to-r from-blue-400 to-blue-600 shadow-ld p-8 z-10"
|
||||||
|
>
|
||||||
|
<div class="absolute inset-px rounded-[calc(var(--radius)-1px)]">
|
||||||
|
<div class="flex justify-center w-full h-full">
|
||||||
|
<SpotlightCard
|
||||||
|
class="w-full p-2 rounded-[--radius] [--radius:theme(borderRadius.lg)] opacity-70"
|
||||||
|
from="#60a5fa"
|
||||||
|
:size="200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative z-20 flex flex-col items-center gap-4 pb-1">
|
||||||
|
<h2 class="text-xl md:text-2xl text-center font-medium text-white">
|
||||||
|
{{ innerJson.title ? innerJson.title : 'Ready to upgrade your OpnForm forms?' }}
|
||||||
|
</h2>
|
||||||
|
<UButton
|
||||||
|
to="/register"
|
||||||
|
color="white"
|
||||||
|
class="hover:no-underline"
|
||||||
|
icon="i-heroicons-arrow-right"
|
||||||
|
trailing
|
||||||
|
>
|
||||||
|
Try OpnForm for free
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { blockProps } from 'vue-notion'
|
||||||
|
import useNotionBlock from '~/components/pages/notion/useNotionBlock.js'
|
||||||
|
|
||||||
|
const props = defineProps(blockProps)
|
||||||
|
|
||||||
|
const block = useNotionBlock(props)
|
||||||
|
const innerJson = computed(() => block.innerJson.value)
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
export default function useNotionBlock (props) {
|
||||||
|
|
||||||
|
const block = computed(() => {
|
||||||
|
const id = props.contentId || Object.keys(props.blockMap)[0]
|
||||||
|
return props.blockMap[id]
|
||||||
|
})
|
||||||
|
|
||||||
|
const value = computed(() => {
|
||||||
|
return block.value?.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const format = computed(() => {
|
||||||
|
return value.value?.format
|
||||||
|
})
|
||||||
|
|
||||||
|
const icon = computed(() => {
|
||||||
|
return format.value?.page_icon || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const width = computed(() => {
|
||||||
|
return format.value?.block_width
|
||||||
|
})
|
||||||
|
|
||||||
|
const properties = computed(() => {
|
||||||
|
return value.value?.properties
|
||||||
|
})
|
||||||
|
|
||||||
|
const caption = computed(() => {
|
||||||
|
return properties.value?.caption
|
||||||
|
})
|
||||||
|
|
||||||
|
const description = computed(() => {
|
||||||
|
return properties.value?.description
|
||||||
|
})
|
||||||
|
|
||||||
|
const src = computed(() => {
|
||||||
|
return mapImageUrl(properties.value?.source[0][0], block.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const title = computed(() => {
|
||||||
|
return properties.value?.title
|
||||||
|
})
|
||||||
|
|
||||||
|
const alt = computed(() => {
|
||||||
|
return caption.value?.[0][0]
|
||||||
|
})
|
||||||
|
|
||||||
|
const type = computed(() => {
|
||||||
|
return value.value?.type
|
||||||
|
})
|
||||||
|
|
||||||
|
const visible = computed(() => {
|
||||||
|
return !props.hideList.includes(type.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasPageLinkOptions = computed(() => {
|
||||||
|
return props.pageLinkOptions?.component && props.pageLinkOptions?.href
|
||||||
|
})
|
||||||
|
|
||||||
|
const parent = computed(() => {
|
||||||
|
return props.blockMap[value.value?.parent_id]
|
||||||
|
})
|
||||||
|
|
||||||
|
const innerJson = computed(() => {
|
||||||
|
if (type.value !== 'code') return
|
||||||
|
if (properties.value.language.flat('Infinity').join('') !== 'JSON') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(
|
||||||
|
title.value.flat(Infinity).join('').replace(/\n/g, '').replace(/\t/g, '').trim()
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse JSON',
|
||||||
|
error,
|
||||||
|
title.value.flat(Infinity).join('').replace(/\n/g, '').replace(/\t/g, '').trim()
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function mapImageUrl (source) {
|
||||||
|
// Implement your mapImageUrl logic here
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
icon,
|
||||||
|
width,
|
||||||
|
properties,
|
||||||
|
caption,
|
||||||
|
description,
|
||||||
|
src,
|
||||||
|
title,
|
||||||
|
alt,
|
||||||
|
block,
|
||||||
|
value,
|
||||||
|
format,
|
||||||
|
type,
|
||||||
|
visible,
|
||||||
|
hasPageLinkOptions,
|
||||||
|
parent,
|
||||||
|
innerJson
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
<template>
|
||||||
|
<div class="relative">
|
||||||
|
<div
|
||||||
|
v-if="(page && page.blocks && published) || loading"
|
||||||
|
class="w-full flex justify-center"
|
||||||
|
>
|
||||||
|
<div class="w-full md:max-w-3xl md:mx-auto px-4 pt-8 md:pt-16 pb-10">
|
||||||
|
<p class="mb-4 text-sm">
|
||||||
|
<UButton
|
||||||
|
:to="{ name: 'integrations' }"
|
||||||
|
variant="ghost"
|
||||||
|
color="gray"
|
||||||
|
class="mb-4"
|
||||||
|
icon="i-heroicons-arrow-left"
|
||||||
|
>
|
||||||
|
Other Integrations
|
||||||
|
</UButton>
|
||||||
|
</p>
|
||||||
|
<h1 class="text-3xl mb-2">
|
||||||
|
{{ page.Title }}
|
||||||
|
</h1>
|
||||||
|
<NotionPage
|
||||||
|
:block-map="page.blocks"
|
||||||
|
:loading="loading"
|
||||||
|
:block-overrides="blockOverrides"
|
||||||
|
:map-page-url="mapPageUrl"
|
||||||
|
/>
|
||||||
|
<p class="text-sm">
|
||||||
|
<NuxtLink
|
||||||
|
:to="{ name: 'integrations' }"
|
||||||
|
class="text-blue-500 hover:text-blue-700 inline-block"
|
||||||
|
>
|
||||||
|
Discover our other Integrations
|
||||||
|
</NuxtLink>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="w-full md:max-w-3xl md:mx-auto px-4 pt-8 md:pt-16 pb-10"
|
||||||
|
>
|
||||||
|
<h1 class="text-3xl">
|
||||||
|
Whoops - Page not found
|
||||||
|
</h1>
|
||||||
|
<UButton
|
||||||
|
:to="{name: 'index'}"
|
||||||
|
class="mt-4"
|
||||||
|
label="Go Home"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<OpenFormFooter class="border-t" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import CustomBlock from '~/components/pages/notion/CustomBlock.vue'
|
||||||
|
import { useNotionCmsStore } from '~/stores/notion_cms.js'
|
||||||
|
|
||||||
|
const blockOverrides = { code: CustomBlock }
|
||||||
|
const slug = computed(() => useRoute().params.slug)
|
||||||
|
const dbId = '1eda631bec208005bd8ed9988b380263'
|
||||||
|
|
||||||
|
const notionCmsStore = useNotionCmsStore()
|
||||||
|
const loading = computed(() => notionCmsStore.loading)
|
||||||
|
|
||||||
|
await notionCmsStore.loadDatabase(dbId)
|
||||||
|
await notionCmsStore.loadPageBySlug(slug.value)
|
||||||
|
|
||||||
|
const page = notionCmsStore.pageBySlug(slug.value)
|
||||||
|
const published = computed(() => {
|
||||||
|
if (!page.value) return false
|
||||||
|
return page.value.Published ?? page.value.published ?? false
|
||||||
|
})
|
||||||
|
|
||||||
|
const mapPageUrl = (pageId) => {
|
||||||
|
// Get everything before the ?
|
||||||
|
pageId = pageId.split('?')[0]
|
||||||
|
const page = notionCmsStore.pages[pageId]
|
||||||
|
const slug = page.slug ?? page.Slug ?? null
|
||||||
|
return useRouter().resolve({ name: 'integrations', params: { slug } }).href
|
||||||
|
}
|
||||||
|
|
||||||
|
defineRouteRules({
|
||||||
|
swr: 3600
|
||||||
|
})
|
||||||
|
definePageMeta({
|
||||||
|
stickyNavbar: true,
|
||||||
|
middleware: ["self-hosted"]
|
||||||
|
})
|
||||||
|
|
||||||
|
useOpnSeoMeta({
|
||||||
|
title: () => page.value.Name,
|
||||||
|
description: () => page.value['Summary - SEO description'] ?? 'Create beautiful forms for free. Unlimited fields, unlimited submissions.'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,239 @@
|
||||||
|
<template>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="mt-2 flex flex-col">
|
||||||
|
<div
|
||||||
|
v-if="loading"
|
||||||
|
class="bg-white py-12 px-4 sm:px-6 lg:px-8"
|
||||||
|
>
|
||||||
|
<loader class="mx-auto h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="bg-white py-12 px-4 sm:px-6 lg:px-8"
|
||||||
|
>
|
||||||
|
<div class="max-w-6xl mx-auto">
|
||||||
|
<h1 class="text-3xl font-bold text-center text-gray-900">
|
||||||
|
Available Integrations
|
||||||
|
</h1>
|
||||||
|
<p class="text-center text-gray-600 mt-2 mb-10">
|
||||||
|
Explore our powerful Integrations
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<div
|
||||||
|
v-for="integration in integrationsList"
|
||||||
|
:key="integration.title"
|
||||||
|
class="relative rounded-2xl bg-gray-50 p-6 shadow border border-gray-200 hover:shadow-lg transition-all duration-300 hover:bg-white"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="`/integrations/${integration.slug}`"
|
||||||
|
class="absolute inset-0"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="integration.popular"
|
||||||
|
class="absolute -top-2 -left-3 -rotate-12 bg-blue-500 text-white text-xs font-semibold px-2 py-1 rounded-md shadow"
|
||||||
|
>
|
||||||
|
Most Popular
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div class="w-10 h-10 bg-white border border-gray-200 rounded-xl flex items-center justify-center">
|
||||||
|
<Icon
|
||||||
|
:name="integration.icon"
|
||||||
|
class="w-8 h-8"
|
||||||
|
dynamic
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-sm text-blue-500 font-medium hover:underline flex items-center gap-1"
|
||||||
|
>
|
||||||
|
Setup Guide
|
||||||
|
<Icon
|
||||||
|
name="heroicons:arrow-top-right-on-square"
|
||||||
|
class="w-4 h-4 flex-shrink-0"
|
||||||
|
dynamic
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="mt-4 text-lg font-semibold text-gray-900">
|
||||||
|
{{ integration.title }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">
|
||||||
|
{{ integration.description }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul class="mt-4 space-y-2 text-sm text-gray-700">
|
||||||
|
<li
|
||||||
|
v-for="step in integration.steps"
|
||||||
|
:key="step"
|
||||||
|
class="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span class="text-green-500">✔</span> {{ step }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="bg-white p-10 max-w-6xl mx-auto">
|
||||||
|
<h2 class="text-4xl font-bold text-center text-gray-900 mb-2">
|
||||||
|
Integration General Setup Guides
|
||||||
|
</h2>
|
||||||
|
<p class="text-center text-gray-600 mb-12">
|
||||||
|
This can be another text
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-10 text-gray-800 max-w-6xl mx-auto">
|
||||||
|
<div
|
||||||
|
v-for="guide in setupGuides"
|
||||||
|
:key="guide.title"
|
||||||
|
>
|
||||||
|
<h2 class="text-xl font-semibold mb-4">
|
||||||
|
{{ guide.title }}
|
||||||
|
</h2>
|
||||||
|
<ol class="space-y-4 text-base">
|
||||||
|
<li
|
||||||
|
v-for="(step, index) in guide.steps"
|
||||||
|
:key="step"
|
||||||
|
class="flex items-start gap-3"
|
||||||
|
>
|
||||||
|
<span class="w-8 h-8 flex items-center justify-center rounded-full bg-blue-100 text-blue-700 font-bold">{{ index + 1 }}</span>
|
||||||
|
<span v-html="step" />
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-[#f4f9ff] max-w-6xl mx-auto rounded-3xl m-10 p-10 flex justify-between items-center">
|
||||||
|
<div class="max-w-md">
|
||||||
|
<h2 class="text-3xl font-bold text-gray-900">
|
||||||
|
Need help?
|
||||||
|
</h2>
|
||||||
|
<p class="mt-2 text-gray-500 text-lg">
|
||||||
|
Visit our Help Center for detailed documentation!
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="inline-flex items-center gap-2 mt-6 px-4 py-2 bg-blue-500 text-white text-sm font-semibold rounded-lg shadow hover:bg-blue-600 transition"
|
||||||
|
@click.prevent="crisp.openHelpdesk()"
|
||||||
|
>
|
||||||
|
Help Center
|
||||||
|
<Icon
|
||||||
|
name="heroicons:arrow-top-right-on-square"
|
||||||
|
class="w-4 h-4 flex-shrink-0"
|
||||||
|
dynamic
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="hidden lg:grid grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="bg-white p-4 rounded-2xl shadow w-64 h-20">
|
||||||
|
<div class="bg-gray-200 w-24 h-3 mb-2 rounded" />
|
||||||
|
<div class="bg-gray-200 w-full h-6 rounded-full" />
|
||||||
|
</div>
|
||||||
|
<div class="bg-white p-4 rounded-2xl shadow w-64 h-20">
|
||||||
|
<div class="bg-gray-200 w-24 h-3 mb-2 rounded" />
|
||||||
|
<div class="bg-gray-200 w-full h-6 rounded-full" />
|
||||||
|
</div>
|
||||||
|
<div class="bg-white p-4 rounded-2xl shadow w-64 h-20">
|
||||||
|
<div class="bg-gray-200 w-24 h-3 mb-2 rounded" />
|
||||||
|
<div class="bg-gray-200 w-full h-6 rounded-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-4 pt-8">
|
||||||
|
<div class="bg-white p-4 rounded-2xl shadow w-64 h-20">
|
||||||
|
<div class="bg-gray-200 w-24 h-3 mb-2 rounded" />
|
||||||
|
<div class="bg-gray-200 w-full h-6 rounded-full" />
|
||||||
|
</div>
|
||||||
|
<div class="bg-white p-4 rounded-2xl shadow w-64 h-20">
|
||||||
|
<div class="bg-gray-200 w-24 h-3 mb-2 rounded" />
|
||||||
|
<div class="bg-gray-200 w-full h-6 rounded-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<OpenFormFooter />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useNotionCmsStore } from '~/stores/notion_cms.js'
|
||||||
|
|
||||||
|
useOpnSeoMeta({
|
||||||
|
title: 'Integrations',
|
||||||
|
description:
|
||||||
|
'Create beautiful forms for free. Unlimited fields, unlimited submissions.'
|
||||||
|
})
|
||||||
|
defineRouteRules({
|
||||||
|
swr: 3600
|
||||||
|
})
|
||||||
|
definePageMeta({
|
||||||
|
stickyNavbar: true,
|
||||||
|
middleware: ["self-hosted"]
|
||||||
|
})
|
||||||
|
|
||||||
|
const crisp = useCrisp()
|
||||||
|
|
||||||
|
const dbId = '1eda631bec208005bd8ed9988b380263'
|
||||||
|
const notionCmsStore = useNotionCmsStore()
|
||||||
|
const loading = computed(() => notionCmsStore.loading)
|
||||||
|
await notionCmsStore.loadDatabase(dbId)
|
||||||
|
const pages = notionCmsStore.databasePages(dbId)
|
||||||
|
|
||||||
|
const integrationsList = computed(() => {
|
||||||
|
if (!pages.value) return []
|
||||||
|
return Object.values(pages.value).filter(page => page.Published).map(page => ({
|
||||||
|
title: page['Integration Name'] ?? page.Name,
|
||||||
|
description: page.Summary ?? '',
|
||||||
|
icon: page.Icon ?? 'i-heroicons-envelope-20-solid',
|
||||||
|
slug: page.slug,
|
||||||
|
steps: (page.Steps) ? page.Steps.split('\n') : [],
|
||||||
|
popular: page['Most Popular'] ?? false
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const setupGuides = [
|
||||||
|
{
|
||||||
|
title: 'Email Integration Setup',
|
||||||
|
steps: [
|
||||||
|
'Navigate to <b>OpnForm</b> > <b>Integrations</b>.',
|
||||||
|
'Select <b>Email</b> and configure SMTP settings.',
|
||||||
|
'Set up email rules for notifications.',
|
||||||
|
'Save & activate email alerts.'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Slack Integration Setup',
|
||||||
|
steps: [
|
||||||
|
'Navigate to <b>OpnForm</b> > <b>Integrations</b>.',
|
||||||
|
'Select <b>Slack</b> and authorize your workspace.',
|
||||||
|
'Choose a channel & customize messages.',
|
||||||
|
'Save & activate Slack alerts.'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'WebHook Integration Setup',
|
||||||
|
steps: [
|
||||||
|
'Navigate to <b>OpnForm</b> > <b>Integrations</b>.',
|
||||||
|
'Select <b>WebHook</b> and enter your endpoint URL.',
|
||||||
|
'Map fields & configure triggers.',
|
||||||
|
'Save & activate WebHook alerts.'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang='scss'>
|
||||||
|
.integration-page {
|
||||||
|
.notion-asset-wrapper {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
Privacy Policy
|
Privacy Policy
|
||||||
</h1>
|
</h1>
|
||||||
<NotionPage
|
<NotionPage
|
||||||
:block-map="blockMap"
|
:block-map="page.blocks"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -16,26 +16,19 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useNotionPagesStore } from "~/stores/notion_pages.js"
|
|
||||||
import { computed } from "vue"
|
|
||||||
|
|
||||||
useOpnSeoMeta({
|
|
||||||
title: "Privacy Policy",
|
|
||||||
})
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ["self-hosted"]
|
middleware: ["self-hosted"]
|
||||||
})
|
})
|
||||||
|
useOpnSeoMeta({
|
||||||
|
title: "Privacy Policy",
|
||||||
|
})
|
||||||
defineRouteRules({
|
defineRouteRules({
|
||||||
swr: 3600,
|
swr: 3600
|
||||||
})
|
})
|
||||||
|
|
||||||
const notionPageStore = useNotionPagesStore()
|
const pageId = '9c97349ceda7455aab9b341d1ff70f79'
|
||||||
await notionPageStore.load("9c97349ceda7455aab9b341d1ff70f79")
|
const notionCmsStore = useNotionCmsStore()
|
||||||
|
const loading = computed(() => notionCmsStore.loading)
|
||||||
const loading = computed(() => notionPageStore.loading)
|
await notionCmsStore.loadPage(pageId)
|
||||||
const blockMap = computed(() =>
|
const page = notionCmsStore.getPage(pageId)
|
||||||
notionPageStore.getByKey("9c97349ceda7455aab9b341d1ff70f79"),
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
Terms & Conditions
|
Terms & Conditions
|
||||||
</h1>
|
</h1>
|
||||||
<NotionPage
|
<NotionPage
|
||||||
:block-map="blockMap"
|
:block-map="page.blocks"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -16,26 +16,19 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useNotionPagesStore } from "~/stores/notion_pages.js"
|
|
||||||
import { computed } from "vue"
|
|
||||||
|
|
||||||
useOpnSeoMeta({
|
|
||||||
title: "Terms & Conditions",
|
|
||||||
})
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ["self-hosted"]
|
middleware: ["self-hosted"]
|
||||||
})
|
})
|
||||||
|
useOpnSeoMeta({
|
||||||
|
title: "Terms & Conditions",
|
||||||
|
})
|
||||||
defineRouteRules({
|
defineRouteRules({
|
||||||
swr: 3600,
|
swr: 3600
|
||||||
})
|
})
|
||||||
|
|
||||||
const notionPageStore = useNotionPagesStore()
|
const pageId = '246420da2834480ca04047b0c5a00929'
|
||||||
await notionPageStore.load("246420da2834480ca04047b0c5a00929")
|
const notionCmsStore = useNotionCmsStore()
|
||||||
|
const loading = computed(() => notionCmsStore.loading)
|
||||||
const loading = computed(() => notionPageStore.loading)
|
await notionCmsStore.loadPage(pageId)
|
||||||
const blockMap = computed(() =>
|
const page = notionCmsStore.getPage(pageId)
|
||||||
notionPageStore.getByKey("246420da2834480ca04047b0c5a00929"),
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import opnformConfig from "~/opnform.config.js"
|
||||||
|
|
||||||
|
function notionApiFetch (entityId, type = 'table') {
|
||||||
|
const apiUrl = opnformConfig.notion.worker
|
||||||
|
return useFetch(`${apiUrl}/${type}/${entityId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchNotionDatabasePages (databaseId) {
|
||||||
|
return notionApiFetch(databaseId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchNotionPageContent (pageId) {
|
||||||
|
return notionApiFetch(pageId, 'page')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useNotionCmsStore = defineStore('notion_cms', () => {
|
||||||
|
|
||||||
|
// State
|
||||||
|
const databases = ref({})
|
||||||
|
const pages = ref({})
|
||||||
|
const pageContents = ref({})
|
||||||
|
const slugToIdMap = ref({})
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const loadDatabase = (databaseId) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (databases.value[databaseId]) return resolve()
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
return fetchNotionDatabasePages(databaseId).then((response) => {
|
||||||
|
databases.value[databaseId] = response.data.value.map(page => formatId(page.id))
|
||||||
|
response.data.value.forEach(page => {
|
||||||
|
pages.value[formatId(page.id)] = {
|
||||||
|
...page,
|
||||||
|
id: formatId(page.id)
|
||||||
|
}
|
||||||
|
const slug = page.Slug ?? page.slug ?? null
|
||||||
|
if (slug) {
|
||||||
|
setSlugToIdMap(slug, page.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
loading.value = false
|
||||||
|
resolve()
|
||||||
|
}).catch((error) => {
|
||||||
|
loading.value = false
|
||||||
|
console.error(error)
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const loadPage = async (pageId) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (pageContents.value[pageId]) return resolve('in already here')
|
||||||
|
loading.value = true
|
||||||
|
return fetchNotionPageContent(pageId).then((response) => {
|
||||||
|
pageContents.value[formatId(pageId)] = response.data.value
|
||||||
|
loading.value = false
|
||||||
|
return resolve('in finishg')
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
loading.value = false
|
||||||
|
return reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadPageBySlug = (slug) => {
|
||||||
|
if (!slugToIdMap.value[slug.toLowerCase()]) return
|
||||||
|
loadPage(slugToIdMap.value[slug.toLowerCase()])
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatId = (id) => id.replaceAll('-', '')
|
||||||
|
|
||||||
|
const getPage = (pageId) => {
|
||||||
|
return {
|
||||||
|
...pages.value[pageId],
|
||||||
|
blocks: getPageBody(pageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPageBody = (pageId) => {
|
||||||
|
return pageContents.value[pageId]
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSlugToIdMap = (slug, pageId) => {
|
||||||
|
if (!slug) return
|
||||||
|
slugToIdMap.value[slug.toLowerCase()] = formatId(pageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPageBySlug = (slug) => {
|
||||||
|
if (!slug) return
|
||||||
|
const pageId = slugToIdMap.value[slug.toLowerCase()]
|
||||||
|
return getPage(pageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
const databasePages = (databaseId) => computed(() => databases.value[databaseId]?.map(id => pages.value[id]) || [])
|
||||||
|
const pageContent = (pageId) => computed(() => pageContents.value[pageId])
|
||||||
|
const pageBySlug = (slug) => computed(() => getPageBySlug(slug))
|
||||||
|
|
||||||
|
return {
|
||||||
|
// state
|
||||||
|
loading,
|
||||||
|
databases,
|
||||||
|
pages,
|
||||||
|
pageContents,
|
||||||
|
slugToIdMap,
|
||||||
|
|
||||||
|
// actions
|
||||||
|
loadDatabase,
|
||||||
|
loadPage,
|
||||||
|
loadPageBySlug,
|
||||||
|
getPage,
|
||||||
|
getPageBody,
|
||||||
|
setSlugToIdMap,
|
||||||
|
getPageBySlug,
|
||||||
|
|
||||||
|
// getters
|
||||||
|
databasePages,
|
||||||
|
pageContent,
|
||||||
|
pageBySlug
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import { defineStore } from "pinia"
|
|
||||||
import { useContentStore } from "~/composables/stores/useContentStore.js"
|
|
||||||
import opnformConfig from "~/opnform.config.js"
|
|
||||||
export const useNotionPagesStore = defineStore("notion_pages", () => {
|
|
||||||
const contentStore = useContentStore()
|
|
||||||
|
|
||||||
const load = (pageId) => {
|
|
||||||
contentStore.startLoading()
|
|
||||||
|
|
||||||
const apiUrl = opnformConfig.notion.worker
|
|
||||||
return useFetch(`${apiUrl}/page/${pageId}`)
|
|
||||||
.then(({ data }) => {
|
|
||||||
const val = data.value
|
|
||||||
val["id"] = pageId
|
|
||||||
contentStore.save(val)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
contentStore.stopLoading()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...contentStore,
|
|
||||||
load,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
Loading…
Reference in New Issue