Skip to content
Open
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
34 changes: 34 additions & 0 deletions docs/.vitepress/theme/CTAButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<template>
<p class="cta">
<a
:class="{
button: true,
'is-large': true,
'is-big': true
}"
:href="`https://account.cg-wire.com/signup`"
>
Try Kitsu For Free
</a>
</p>
</template>

<style scoped>
.cta {
background-color: #00b242;
max-width: 400px;
margin-left: auto;
margin-right: auto;
text-align: center;
padding: 30px 10px 30px 10px;
margin-top: 30px;
margin-bottom: 30px;
border-radius: 25px;
font-size: 25px;
}

.cta a {
color: white;
text-decoration: none;
}
</style>
220 changes: 220 additions & 0 deletions docs/.vitepress/theme/ExitIntentPopup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
<template>
<!--
ClientOnly is required here: VitePress prerenders on the server,
where `document` doesn't exist and Teleport-to-body has no target.
This whole block is skipped during SSG and only mounts in the browser.
-->
<ClientOnly>
<Teleport v-if="variant === 'variant_b'" to="body">
<Transition name="modal">
<div
v-if="visible"
class="overlay"
@click.self="close"
>
<div class="popup">
<button class="close-btn" @click="close">×</button>

<div class="content">
<div class="left">
<div class="badge">{{ badgeText }}</div>

<h2>Before you go...</h2>

<p>
Ready to level up your production workflow? Create your
free account and get access to the latest features from
CGWire's Kitsu.
</p>

<a
class="navbar-item signup"
:href="signupUrl"
>
Signup
</a>
</div>

<div class="right">
<div class="illustration">
<img src="/logo-kitsu.svg" width="100px"/>
</div>
</div>
</div>
</div>
</div>
</Transition>
</Teleport>
</ClientOnly>
</template>

<script setup>
import { onMounted, onBeforeUnmount, ref, computed } from "vue";
import { useData } from "vitepress";
import { useABTest } from "./composables/useABTests.js";

// VitePress's built-in reactive site data (replaces $i18n.locale)
const { lang } = useData();

const variant = useABTest("exit_intent_popup_test", [
{ name: "control", weight: 50 },
{ name: "variant_b", weight: 50 },
]);

const visible = ref(false);

// Swap in vitepress-plugin-i18n's t() here if you have one installed;
// otherwise this is a plain fallback string.
const badgeText = "Free Trial";

const signupUrl = computed(
() => `https://account.cg-wire.com/signup?locale=${lang.value}`
);

function showPopup() {
const alreadySeen = sessionStorage.getItem("exit-popup");

if (!alreadySeen) {
visible.value = true;
sessionStorage.setItem("exit-popup", true);
}
}

function close() {
visible.value = false;
}

function handleMouseLeave(e) {
if (e.clientY <= 0 && !visible.value) {
showPopup();
}
}

onMounted(() => {
document.addEventListener("mouseleave", handleMouseLeave);
});

onBeforeUnmount(() => {
document.removeEventListener("mouseleave", handleMouseLeave);
});
</script>

<style scoped>
.overlay {
position: fixed;
inset: 0;
background: rgba(7, 11, 19, 0.82);
backdrop-filter: blur(6px);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}

.popup {
width: min(720px, 92vw);
background: #fff;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
position: relative;
}

.content {
display: flex;
}

.left {
flex: 1;
padding: 42px;
background: white;
}

.right {
width: 220px;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #2ecc71, #27ae60);
}

.illustration {
font-size: 72px;
color: white;
}

.badge {
display: inline-block;
padding: 6px 12px;
border-radius: 999px;
background: rgba(46, 204, 113, 0.12);
color: #1f8f4a;
font-size: 13px;
font-weight: 600;
margin-bottom: 18px;
}

h2 {
color: #1f2937;
font-size: 34px;
margin-bottom: 12px;
}

p {
color: #6b7280;
line-height: 1.6;
margin-bottom: 24px;
}

.signup {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 14px 24px;
border-radius: 12px;
background: #2ecc71;
color: white;
font-weight: 600;
text-decoration: none;
transition: all 0.2s ease;
}

.signup:hover {
background: #27ae60;
transform: translateY(-1px);
}

.close-btn {
position: absolute;
top: 12px;
right: 12px;
width: 36px;
height: 36px;
border: none;
border-radius: 50%;
background: #f3f4f6;
color: #6b7280;
cursor: pointer;
transition: all 0.2s ease;
}

.close-btn:hover {
background: #e5e7eb;
color: #111827;
}

.modal-enter-active,
.modal-leave-active {
transition: all 0.25s ease;
}

.modal-enter-from,
.modal-leave-to {
opacity: 0;
}

.modal-enter-from .popup,
.modal-leave-to .popup {
transform: scale(0.95);
}
</style>
5 changes: 5 additions & 0 deletions docs/.vitepress/theme/KitsuLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import DefaultTheme from 'vitepress/theme'

import NavbarSignupButton from "./NavbarSignupButton.vue"
import ExitIntentPopup from "./ExitIntentPopup.vue"

const { Layout } = DefaultTheme
</script>
Expand All @@ -11,5 +12,9 @@ const { Layout } = DefaultTheme
<template #nav-bar-content-after>
<NavbarSignupButton/>
</template>

<template #layout-bottom>
<ExitIntentPopup/>
</template>
</Layout>
</template>
56 changes: 56 additions & 0 deletions docs/.vitepress/theme/composables/useABTests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// .vitepress/theme/composables/useABTests.js
import { ref, onMounted } from "vue";

function pickWeightedVariant(variants) {
const total = variants.reduce((sum, v) => sum + v.weight, 0);
let r = Math.random() * total;
for (const v of variants) {
if (r < v.weight) return v.name;
r -= v.weight;
}
return variants[variants.length - 1].name;
}

function readCookie(name) {
const match = document.cookie.match(
new RegExp(`(?:^|; )${name}=([^;]*)`)
);
return match ? decodeURIComponent(match[1]) : null;
}

function writeCookie(name, value, maxAgeSeconds) {
document.cookie = `${name}=${encodeURIComponent(
value
)}; path=/; max-age=${maxAgeSeconds}; SameSite=Lax`;
}

/**
* VitePress has no per-request server (pages are prerendered once at
* build time), so there's no SSR-aware cookie API like Nuxt's useCookie.
* All cookie reads/writes and Matomo tracking are deferred to onMounted
* so nothing touches `document`/`window` during the static build.
* `variant` is `null` until then, which is fine since the popup itself
* is already wrapped in <ClientOnly>.
*/
export function useABTest(testId, variants) {
const cookieName = `ab_${testId}`;
const variant = ref(null);

onMounted(() => {
let value = readCookie(cookieName);

if (!value) {
value = pickWeightedVariant(variants);
writeCookie(cookieName, value, 60 * 60 * 24 * 90); // 90 days
}

variant.value = value;

if (window._paq) {
window._paq.push(["setCustomDimension", 1, value]);
window._paq.push(["trackEvent", "AB Test", "1", value]);
}
});

return variant;
}
1 change: 1 addition & 0 deletions docs/public/logo-kitsu.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 9 additions & 1 deletion docs/self-hosting/vs-cloud-hosting.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
<script setup>
import CTAButton from '../.vitepress/theme/CTAButton.vue'
</script>

# Self-hosting vs Cloud Hosting

Choosing how to host Kitsu has a direct impact on setup time, operational overhead, scalability, and long-term flexibility. This page outlines the key differences between cloud-hosted Kitsu and self-hosted Kitsu to help animation studios decide which approach best fits their needs.
Expand All @@ -9,6 +13,8 @@ Both options provide the same core features, but differ significantly in how muc

Alternatively, you can also [consider on-premise cloud hosting](https://www.cg-wire.com/pricing) to get the best of both worlds.

<CTAButton/>

## Deployment & Setup

### Cloud Hosting
Expand Down Expand Up @@ -240,4 +246,6 @@ Provides maximum portability.
| **Portability** | Simple migration within supported paths | Maximum portability across environments |
| **Support** | Priority support included | No official support if conflicts occur |

### Choose [On-Premise Cloud Hosting](https://www.cg-wire.com/pricing) if you want the best of both
### Choose [On-Premise Cloud Hosting](https://www.cg-wire.com/pricing) if you want the best of both worlds.

<CTAButton/>
6 changes: 6 additions & 0 deletions docs/start-here/cloud-hosting.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
<script setup>
import CTAButton from '../.vitepress/theme/CTAButton.vue'
</script>

# Cloud Hosting

Kitsu is a web application that requires a server to run. You can either host
Expand Down Expand Up @@ -33,6 +37,8 @@ gazu.log_in("me@studio.com", "password")
projects = gazu.project.all_open_projects()
```

<CTAButton/>

## What's included

- **Automatic updates** -- always on the latest Kitsu version
Expand Down
6 changes: 6 additions & 0 deletions docs/start-here/dev-quickstart.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
<script setup>
import CTAButton from '../.vitepress/theme/CTAButton.vue'
</script>

# Developer Quickstart

## 1. Kitsu API Server Installation
Expand All @@ -8,6 +12,8 @@ There are three options to install a Kitsu API server:
1. [Self-hosting](/self-hosting/vs-cloud-hosting) - if you have a sysops team to manage your servers.
1. [Docker](/start-here/docker) - to set up a local dev environment for testing.

<CTAButton/>

## 2. Setting up the API client

If you're using a SDK, install it first:
Expand Down
Loading