Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# PostHog Analytics
VITE_POSTHOG_KEY=

# Algolia Search
VITE_ALGOLIA_APP_ID=
VITE_ALGOLIA_API_KEY=
VITE_ALGOLIA_INDEX_NAME=
Comment on lines +4 to +7
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reorder Algolia keys to satisfy dotenv-linter.

Proposed fix
 # Algolia Search
-VITE_ALGOLIA_APP_ID=
 VITE_ALGOLIA_API_KEY=
+VITE_ALGOLIA_APP_ID=
 VITE_ALGOLIA_INDEX_NAME=
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Algolia Search
VITE_ALGOLIA_APP_ID=
VITE_ALGOLIA_API_KEY=
VITE_ALGOLIA_INDEX_NAME=
# Algolia Search
VITE_ALGOLIA_API_KEY=
VITE_ALGOLIA_APP_ID=
VITE_ALGOLIA_INDEX_NAME=
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 6-6: [UnorderedKey] The VITE_ALGOLIA_API_KEY key should go before the VITE_ALGOLIA_APP_ID key

(UnorderedKey)

🤖 Prompt for AI Agents
In @.env.example around lines 4 - 7, The dotenv-linter complains about unsorted
keys in .env.example; reorder the Algolia variables VITE_ALGOLIA_APP_ID,
VITE_ALGOLIA_API_KEY, and VITE_ALGOLIA_INDEX_NAME into alphabetical order (or
the project’s required ordering) so the three entries are sorted consistently in
the file; update the block containing those symbols so the linter no longer
flags them.

5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@ examples-temp
node_modules
pnpm-global
TODOs.md
*.timestamp-*.mjs
*.timestamp-*.mjs
.env
.env.local
.env.*.local
63 changes: 59 additions & 4 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,47 @@
import { defineConfig } from 'vitepress'
import { defineConfig, type HeadConfig } from 'vitepress'
import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs'
import { withMermaid } from 'vitepress-plugin-mermaid'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'

function loadEnvVar(key: string): string | undefined {
// process.env takes precedence (CI/hosting platforms set vars here)
if (key in process.env) return process.env[key] || undefined
// Fall back to .env file for local development
try {
const envFile = readFileSync(resolve(process.cwd(), '.env'), 'utf-8')
const match = envFile.match(new RegExp(`^${key}=(.+)$`, 'm'))
return match?.[1]?.trim()
} catch {
return undefined
}
}

const posthogKey = loadEnvVar('VITE_POSTHOG_KEY')
const algoliaAppId = loadEnvVar('VITE_ALGOLIA_APP_ID')
const algoliaApiKey = loadEnvVar('VITE_ALGOLIA_API_KEY')
const algoliaIndexName = loadEnvVar('VITE_ALGOLIA_INDEX_NAME')

const searchConfig = algoliaAppId && algoliaApiKey && algoliaIndexName
? {
provider: 'algolia' as const,
options: {
appId: algoliaAppId,
apiKey: algoliaApiKey,
indexName: algoliaIndexName,
insights: true
}
}
: { provider: 'local' as const }

const posthogHead: HeadConfig[] = posthogKey
? [
['script', {}, `
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug getPageViewId captureTraceFeedback captureTraceMetric".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('${posthogKey}',{api_host:'https://us.posthog.com', opt_out_capturing_by_default: true, persistence: 'memory'});
`]
]
: []

export default withMermaid(defineConfig({
markdown: {
Expand Down Expand Up @@ -45,6 +86,22 @@ export default withMermaid(defineConfig({
head: [
['link', { rel: 'icon', href: '/logo/favicon-32x32.png' }],

// Google Analytics with Consent Mode v2
['script', { async: '', src: 'https://www.googletagmanager.com/gtag/js?id=G-JF828SKW90' }],
['script', {}, `window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'default', {
'analytics_storage': 'denied',
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied'
});
gtag('js', new Date());
gtag('config', 'G-JF828SKW90');`],

// PostHog Analytics (loaded only when VITE_POSTHOG_KEY is set)
...posthogHead,

// SEO: Basic meta tags
['meta', { name: 'viewport', content: 'width=device-width, initial-scale=1.0' }],
['meta', { name: 'author', content: 'Plane' }],
Expand Down Expand Up @@ -619,9 +676,7 @@ export default withMermaid(defineConfig({
{ icon: 'linkedin', link: 'https://www.linkedin.com/company/planepowers/' }
],

search: {
provider: 'local'
},
search: searchConfig,

editLink: {
pattern: 'https://github.com/makeplane/developer-docs/edit/main/:path'
Expand Down
173 changes: 173 additions & 0 deletions docs/.vitepress/theme/components/CookieConsent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'

const STORAGE_KEY = 'plane-docs-cookie-consent'
const showBanner = ref(false)

function getConsent(): string | null {
try {
return localStorage.getItem(STORAGE_KEY)
} catch {
return null
}
}

function grantConsent() {
if (typeof window === 'undefined') return

// Google Analytics
if (typeof window.gtag === 'function') {
window.gtag('consent', 'update', {
analytics_storage: 'granted'
})
}

// PostHog (may not be loaded if VITE_POSTHOG_KEY is unset)
if (window.posthog?.opt_in_capturing) {
window.posthog.opt_in_capturing()
}
}

function denyConsent() {
if (typeof window === 'undefined') return

// Google Analytics — consent stays denied by default, no update needed

// PostHog (may not be loaded if VITE_POSTHOG_KEY is unset)
if (window.posthog?.opt_out_capturing) {
window.posthog.opt_out_capturing()
}
}

function accept() {
try {
localStorage.setItem(STORAGE_KEY, 'granted')
} catch {}
grantConsent()
showBanner.value = false
}

function decline() {
try {
localStorage.setItem(STORAGE_KEY, 'denied')
} catch {}
denyConsent()
showBanner.value = false
}

onMounted(() => {
const consent = getConsent()
if (consent === 'granted') {
grantConsent()
} else if (consent === 'denied') {
denyConsent()
} else {
showBanner.value = true
}
})
</script>

<template>
<Transition name="consent">
<div v-if="showBanner" class="consent-banner">
<p class="consent-text">
We use cookies and analytics to improve your experience. You can accept or decline non-essential cookies.
</p>
<div class="consent-actions">
<button class="consent-btn consent-btn-decline" @click="decline">Decline</button>
<button class="consent-btn consent-btn-accept" @click="accept">Accept</button>
</div>
</div>
</Transition>
</template>

<style scoped>
.consent-banner {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 100;
display: flex;
align-items: center;
gap: 16px;
max-width: 680px;
width: calc(100% - 32px);
padding: 14px 20px;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
}

.consent-text {
margin: 0;
font-size: 13px;
line-height: 1.5;
color: var(--vp-c-text-2);
flex: 1;
}

.consent-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}

.consent-btn {
padding: 6px 16px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
border: none;
white-space: nowrap;
transition: background 0.15s ease;
}

.consent-btn-decline {
background: transparent;
color: var(--vp-c-text-2);
border: 1px solid var(--vp-c-divider);
}

.consent-btn-decline:hover {
background: var(--vp-c-bg-mute);
}

.consent-btn-accept {
background: var(--vp-c-brand-1);
color: #fff;
}

.consent-btn-accept:hover {
opacity: 0.9;
}

@media (max-width: 540px) {
.consent-banner {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.consent-actions {
justify-content: flex-end;
}
}

/* Transition */
.consent-enter-active {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.consent-leave-active {
transition: opacity 0.2s ease, transform 0.2s ease;
}
.consent-enter-from {
opacity: 0;
transform: translateX(-50%) translateY(20px);
}
.consent-leave-to {
opacity: 0;
transform: translateX(-50%) translateY(20px);
}
</style>
8 changes: 7 additions & 1 deletion docs/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import DefaultTheme from 'vitepress/theme'
import type { Theme } from 'vitepress'
import { onMounted, nextTick } from 'vue'
import { onMounted, nextTick, h } from 'vue'
import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client'

import './style.css'
Expand All @@ -11,6 +11,7 @@ import CodePanel from './components/CodePanel.vue'
import ResponsePanel from './components/ResponsePanel.vue'
import Card from './components/Card.vue'
import CardGroup from './components/CardGroup.vue'
import CookieConsent from './components/CookieConsent.vue'

/**
* Adds 'api-page' class to hide the aside on API reference pages
Expand Down Expand Up @@ -108,6 +109,11 @@ function updateHashOnTabClick(event: Event) {

export default {
extends: DefaultTheme,
Layout() {
return h(DefaultTheme.Layout, null, {
'layout-bottom': () => h(CookieConsent)
})
},
enhanceApp({ app, router }) {
enhanceAppWithTabs(app)

Expand Down
Loading