new selfhosted version

This commit is contained in:
antonio
2025-11-28 14:11:51 +01:00
parent afda29997d
commit 951860f67e
1046 changed files with 72586 additions and 574750 deletions

View File

@@ -0,0 +1,84 @@
<script lang="ts" setup>
const { data: billingAddress, status: billingAddressStatus, refresh: refreshBillingAddress } = useAuthFetch('/api/user/customer');
const currentBillingAddress = ref({
line1: '',
line2: '',
country: '',
postal_code: '',
city: '',
state: ''
})
const canSave = computed(() => {
if (!billingAddress.value) return false;
if (currentBillingAddress.value.line1 !== billingAddress.value.line1) return true;
if (currentBillingAddress.value.line2 !== billingAddress.value.line2) return true;
if (currentBillingAddress.value.country !== billingAddress.value.country) return true;
if (currentBillingAddress.value.postal_code !== billingAddress.value.postal_code) return true;
if (currentBillingAddress.value.city !== billingAddress.value.city) return true;
if (currentBillingAddress.value.state !== billingAddress.value.state) return true;
return false;
})
watch(billingAddress, () => {
if (!billingAddress.value) return;
currentBillingAddress.value.line1 = billingAddress.value.line1;
currentBillingAddress.value.line2 = billingAddress.value.line2;
currentBillingAddress.value.country = billingAddress.value.country;
currentBillingAddress.value.postal_code = billingAddress.value.postal_code;
currentBillingAddress.value.city = billingAddress.value.city;
currentBillingAddress.value.state = billingAddress.value.state;
});
async function updateCustomer() {
await useCatch({
toast: true,
toastTitle: 'Error updating customer',
async action() {
await useAuthFetchSync('/api/user/update_customer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: currentBillingAddress.value
});
await refreshBillingAddress();
},
onSuccess(_, showToast) {
showToast('Update success', { description: 'Customer updated successfully', position: 'top-right' })
},
})
}
</script>
<template>
<div v-if="billingAddressStatus === 'success'" class="flex justify-center flex-col gap-4">
<div class="w-full flex flex-col gap-2">
<Input v-model="currentBillingAddress.line1" placeholder="Address line 1"></Input>
<Input v-model="currentBillingAddress.line2" placeholder="Address line 2"></Input>
<div class="flex gap-2">
<Input v-model="currentBillingAddress.country" placeholder="Country"></Input>
<Input v-model="currentBillingAddress.postal_code" placeholder="Postal code"></Input>
</div>
<div class="flex gap-2">
<Input v-model="currentBillingAddress.city" placeholder="City"></Input>
<Input v-model="currentBillingAddress.state" placeholder="State"></Input>
</div>
</div>
<div class="flex justify-end">
<Button :disabled="!canSave" @click="updateCustomer()" class="w-fit px-10"> Save </Button>
</div>
</div>
<div v-else class="flex justify-center">
<Loader></Loader>
</div>
</template>

View File

@@ -0,0 +1,64 @@
<script lang="ts" setup>
import { FileIcon, FileCheck } from 'lucide-vue-next';
const { data: invoices, status: invoicesStatus } = useAuthFetch('/api/user/invoices');
</script>
<template>
<div class="flex justify-center">
<Card class="w-full" v-if="invoicesStatus === 'success' && invoices">
<CardContent class="flex flex-col gap-4">
<Table>
<TableHeader>
<TableRow class="*:text-center">
<TableHead class="w-[5%]"></TableHead>
<TableHead class="w-fit"> Date </TableHead>
<TableHead class="w-fit"> Price </TableHead>
<TableHead class="w-fit"> Number </TableHead>
<TableHead class="w-fit"> Status </TableHead>
<TableHead class="w-fit"> Actions </TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="invoice of invoices.data" class="h-[2rem]">
<TableCell>
<FileCheck class="size-4"></FileCheck>
</TableCell>
<TableCell>
{{ new Date(invoice.created * 1000).toLocaleString() }}
</TableCell>
<TableCell>
{{ invoice.amount_due / 100 }}
</TableCell>
<TableCell>
{{ invoice.number }}
</TableCell>
<TableCell>
<Badge :class="{
'bg-red-300': invoice.status === 'open'
}">
{{ invoice.status }}
</Badge>
</TableCell>
<TableCell>
<NuxtLink target="_blank" :to="invoice.hosted_invoice_url ?? '#'">
<Button variant="ghost">
<FileIcon></FileIcon>
Manage
</Button>
</NuxtLink>
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
<div v-else class="flex justify-center">
<Loader></Loader>
</div>
</div>
</template>

View File

@@ -0,0 +1,111 @@
<script lang="ts" setup>
import { LoaderCircle, TriangleAlert } from 'lucide-vue-next';
import type { TUserPlanInfo } from '~/server/api/user/plan';
import { getPlanFromId, PREMIUM_PLAN, type PLAN_TAG } from '@data/PLANS';
const { data: planInfo, status: planInfoStatus } = useAuthFetch<TUserPlanInfo>('/api/user/plan', {
key: 'current_plan'
});
const premiumStore = usePremiumStore();
function getPrice(type: number) {
const plan = getPlanFromId(type);
if (!plan) return 'ERROR';
return (plan.COST / 100).toFixed(2).replace('.', ',');
}
const billingPeriodPercent = computed(() => {
if (!planInfo.value) return 0;
const start = planInfo.value.start_at;
const end = planInfo.value.end_at;
const duration = end - start;
const remaining = end - Date.now();
const percent = 100 - Math.floor(100 / duration * remaining);
return percent;
});
const billingDaysRemaining = computed(() => {
if (!planInfo.value) return 0;
const end = planInfo.value.end_at;
const remaining = end - Date.now();
return Math.floor(remaining / (1000 * 60 * 60 * 24))
})
</script>
<template>
<div class="flex justify-center">
<Card class="w-full">
<CardContent>
<div v-if="planInfo && planInfoStatus === 'success'" class="flex flex-col gap-4">
<div class="flex items-center gap-2">
<div class="font-semibold shrink-0">
{{ planInfo.premium ? 'Premium' : 'Free' }} plan
</div>
<Badge variant="outline">
{{ premiumStore.planInfo?.NAME ?? '???' }}
</Badge>
<Tooltip v-if="planInfo.payment_failed">
<TooltipTrigger as-child>
<TriangleAlert class="size-5 text-red-400">
</TriangleAlert>
</TooltipTrigger>
<TooltipContent side="right" align="center">
Please update your billing details to avoid service interruption.
</TooltipContent>
</Tooltip>
<div class="grow"></div>
<div v-if="!isSelfhosted()" class="shrink-0">
<span class="text-[1.3rem] font-semibold"> {{ getPrice(planInfo.premium_type) }}</span>
<span class="text-muted-foreground text-[1.1rem]">
{{ premiumStore.isAnnual ? ' per year' : ' per month' }}
</span>
</div>
</div>
<div class="flex flex-col gap-2">
<div>
Billing period:
</div>
<div class="flex gap-8 items-center">
<Progress class="mt-[1px]" :model-value="billingPeriodPercent"> </Progress>
<div class="shrink-0 font-medium">
{{ billingDaysRemaining }} days left
</div>
</div>
</div>
<Separator></Separator>
<div class="flex items-center justify-between">
<div class="text-muted-foreground">
Expire date: {{ new Date(planInfo.end_at).toLocaleDateString() }}
</div>
<div>
<NuxtLink :to="isSelfhosted() ? 'https://litlyx.com/pricing-selfhosted': '/plans'">
<Button> Upgrade plan </Button>
</NuxtLink>
</div>
</div>
</div>
<div class="flex items-center justify-center" v-else>
<LoaderCircle class="size-10 animate-[spin_1s_ease-in-out_infinite] duration-500">
</LoaderCircle>
</div>
</CardContent>
</Card>
</div>
</template>

View File

@@ -0,0 +1,62 @@
<script lang="ts" setup>
import { LoaderCircle } from 'lucide-vue-next';
import type { TUserPlanInfo } from '~/server/api/user/plan';
import { getPlanFromId, PREMIUM_PLAN, type PLAN_TAG } from '@data/PLANS';
const { data: planInfo, status: planInfoStatus } = useAuthFetch<TUserPlanInfo>('/api/user/plan', {
key: 'current_plan'
});
const usagePercent = computed(() => {
if (!planInfo.value) return 0;
return 100 / planInfo.value.limit * planInfo.value.count;
})
</script>
<template>
<div class="flex justify-center">
<Card class="w-full">
<CardContent>
<div v-if="planInfo && planInfoStatus === 'success'" class="flex flex-col gap-4">
<div class="flex flex-col">
<div class="font-semibold shrink-0">
Usage
</div>
<div class="text-muted-foreground">
Check the usage limits of your project.
</div>
</div>
<div class="flex flex-col gap-2">
<div>
Usage:
</div>
<div class="flex gap-8 items-center">
<Progress class="mt-[1px]" :model-value="Math.floor(usagePercent)"> </Progress>
<div class="shrink-0 font-medium">
{{ usagePercent.toFixed(2) }}%
</div>
</div>
<div class="text-center">
{{ formatNumberK(planInfo.count) }} / {{ formatNumberK(planInfo.limit) }}
</div>
</div>
</div>
<div class="flex items-center justify-center" v-else>
<LoaderCircle class="size-10 animate-[spin_1s_ease-in-out_infinite] duration-500">
</LoaderCircle>
</div>
</CardContent>
</Card>
</div>
</template>