yaxa-svelte 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +163 -0
- package/dist/assets/favicon.svg +15 -0
- package/dist/components/elements/Avatar.svelte +106 -0
- package/dist/components/elements/Avatar.svelte.d.ts +40 -0
- package/dist/components/elements/AvatarGroup.svelte +18 -0
- package/dist/components/elements/AvatarGroup.svelte.d.ts +8 -0
- package/dist/components/elements/Badge.svelte +202 -0
- package/dist/components/elements/Badge.svelte.d.ts +75 -0
- package/dist/components/elements/Button.svelte +300 -0
- package/dist/components/elements/Button.svelte.d.ts +109 -0
- package/dist/components/elements/ButtonGroup.svelte +22 -0
- package/dist/components/elements/ButtonGroup.svelte.d.ts +9 -0
- package/dist/components/elements/Chip.svelte +117 -0
- package/dist/components/elements/Chip.svelte.d.ts +15 -0
- package/dist/components/elements/DataTable.svelte +222 -0
- package/dist/components/elements/DataTable.svelte.d.ts +43 -0
- package/dist/components/elements/Icon.svelte +87 -0
- package/dist/components/elements/Icon.svelte.d.ts +10 -0
- package/dist/components/elements/Kbd.svelte +37 -0
- package/dist/components/elements/Kbd.svelte.d.ts +32 -0
- package/dist/components/elements/Link.svelte +63 -0
- package/dist/components/elements/Link.svelte.d.ts +15 -0
- package/dist/components/elements/Logo.svelte +77 -0
- package/dist/components/elements/Logo.svelte.d.ts +9 -0
- package/dist/components/elements/Meter.svelte +87 -0
- package/dist/components/elements/Meter.svelte.d.ts +14 -0
- package/dist/components/elements/Progress.svelte +63 -0
- package/dist/components/elements/Progress.svelte.d.ts +49 -0
- package/dist/components/elements/Skeleton.svelte +12 -0
- package/dist/components/elements/Skeleton.svelte.d.ts +6 -0
- package/dist/components/elements/Spinner.svelte +39 -0
- package/dist/components/elements/Spinner.svelte.d.ts +7 -0
- package/dist/components/forms/Checkbox.svelte +77 -0
- package/dist/components/forms/Checkbox.svelte.d.ts +17 -0
- package/dist/components/forms/ColorPicker.svelte +142 -0
- package/dist/components/forms/ColorPicker.svelte.d.ts +10 -0
- package/dist/components/forms/Form.svelte +84 -0
- package/dist/components/forms/Form.svelte.d.ts +14 -0
- package/dist/components/forms/FormField.svelte +56 -0
- package/dist/components/forms/FormField.svelte.d.ts +14 -0
- package/dist/components/forms/Input.svelte +138 -0
- package/dist/components/forms/Input.svelte.d.ts +58 -0
- package/dist/components/forms/RadioGroup.svelte +74 -0
- package/dist/components/forms/RadioGroup.svelte.d.ts +17 -0
- package/dist/components/forms/Select.svelte +91 -0
- package/dist/components/forms/Select.svelte.d.ts +50 -0
- package/dist/components/forms/Slider.svelte +37 -0
- package/dist/components/forms/Slider.svelte.d.ts +12 -0
- package/dist/components/forms/Switch.svelte +81 -0
- package/dist/components/forms/Switch.svelte.d.ts +53 -0
- package/dist/components/forms/Textarea.svelte +73 -0
- package/dist/components/forms/Textarea.svelte.d.ts +64 -0
- package/dist/components/layout/Card.svelte +89 -0
- package/dist/components/layout/Card.svelte.d.ts +64 -0
- package/dist/components/layout/Container.svelte +36 -0
- package/dist/components/layout/Container.svelte.d.ts +37 -0
- package/dist/components/layout/Divider.svelte +45 -0
- package/dist/components/layout/Divider.svelte.d.ts +11 -0
- package/dist/components/layout/Footer.svelte +96 -0
- package/dist/components/layout/Footer.svelte.d.ts +10 -0
- package/dist/components/layout/Header.svelte +175 -0
- package/dist/components/layout/Header.svelte.d.ts +12 -0
- package/dist/components/layout/Section.svelte +62 -0
- package/dist/components/layout/Section.svelte.d.ts +14 -0
- package/dist/components/layout/YaxaApp.svelte +48 -0
- package/dist/components/layout/YaxaApp.svelte.d.ts +12 -0
- package/dist/components/navigation/Breadcrumb.svelte +58 -0
- package/dist/components/navigation/Breadcrumb.svelte.d.ts +13 -0
- package/dist/components/navigation/CommandPalette.svelte +201 -0
- package/dist/components/navigation/CommandPalette.svelte.d.ts +19 -0
- package/dist/components/navigation/DropdownMenu.svelte +75 -0
- package/dist/components/navigation/DropdownMenu.svelte.d.ts +20 -0
- package/dist/components/navigation/Pagination.svelte +106 -0
- package/dist/components/navigation/Pagination.svelte.d.ts +10 -0
- package/dist/components/navigation/Tabs.svelte +91 -0
- package/dist/components/navigation/Tabs.svelte.d.ts +18 -0
- package/dist/components/overlays/Accordion.svelte +54 -0
- package/dist/components/overlays/Accordion.svelte.d.ts +16 -0
- package/dist/components/overlays/Alert.svelte +110 -0
- package/dist/components/overlays/Alert.svelte.d.ts +37 -0
- package/dist/components/overlays/ContextMenu.svelte +67 -0
- package/dist/components/overlays/ContextMenu.svelte.d.ts +21 -0
- package/dist/components/overlays/Modal.svelte +99 -0
- package/dist/components/overlays/Modal.svelte.d.ts +40 -0
- package/dist/components/overlays/Popover.svelte +33 -0
- package/dist/components/overlays/Popover.svelte.d.ts +11 -0
- package/dist/components/overlays/Slideover.svelte +102 -0
- package/dist/components/overlays/Slideover.svelte.d.ts +37 -0
- package/dist/components/overlays/Tooltip.svelte +39 -0
- package/dist/components/overlays/Tooltip.svelte.d.ts +12 -0
- package/dist/components/saas/AuthCard.svelte +273 -0
- package/dist/components/saas/AuthCard.svelte.d.ts +16 -0
- package/dist/components/saas/PricingCard.svelte +166 -0
- package/dist/components/saas/PricingCard.svelte.d.ts +19 -0
- package/dist/components/saas/PricingTable.svelte +139 -0
- package/dist/components/saas/PricingTable.svelte.d.ts +11 -0
- package/dist/components/saas/SubscriptionCard.svelte +162 -0
- package/dist/components/saas/SubscriptionCard.svelte.d.ts +16 -0
- package/dist/components/saas/UserMenu.svelte +137 -0
- package/dist/components/saas/UserMenu.svelte.d.ts +19 -0
- package/dist/components/seo/Favicons.svelte +22 -0
- package/dist/components/seo/Favicons.svelte.d.ts +8 -0
- package/dist/components/seo/OgImage.svelte +61 -0
- package/dist/components/seo/OgImage.svelte.d.ts +12 -0
- package/dist/components/seo/Seo.svelte +170 -0
- package/dist/components/seo/Seo.svelte.d.ts +21 -0
- package/dist/composables/useAuth.svelte.d.ts +23 -0
- package/dist/composables/useAuth.svelte.js +157 -0
- package/dist/composables/useClipboard.svelte.d.ts +9 -0
- package/dist/composables/useClipboard.svelte.js +36 -0
- package/dist/composables/useColorMode.svelte.d.ts +8 -0
- package/dist/composables/useColorMode.svelte.js +20 -0
- package/dist/composables/useDebounce.svelte.d.ts +3 -0
- package/dist/composables/useDebounce.svelte.js +17 -0
- package/dist/composables/useMediaQuery.svelte.d.ts +3 -0
- package/dist/composables/useMediaQuery.svelte.js +17 -0
- package/dist/composables/useShortcuts.svelte.d.ts +10 -0
- package/dist/composables/useShortcuts.svelte.js +50 -0
- package/dist/composables/useToast.d.ts +19 -0
- package/dist/composables/useToast.js +34 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +92 -0
- package/dist/server/auth/hook.d.ts +14 -0
- package/dist/server/auth/hook.js +36 -0
- package/dist/server/auth/index.d.ts +39 -0
- package/dist/server/auth/index.js +69 -0
- package/dist/server/db/index.d.ts +18 -0
- package/dist/server/db/index.js +43 -0
- package/dist/server/db/schema-pg.d.ts +977 -0
- package/dist/server/db/schema-pg.js +83 -0
- package/dist/server/db/schema-sqlite.d.ts +1045 -0
- package/dist/server/db/schema-sqlite.js +83 -0
- package/dist/server/db/types.d.ts +44 -0
- package/dist/server/db/types.js +1 -0
- package/dist/server/email/index.d.ts +27 -0
- package/dist/server/email/index.js +79 -0
- package/dist/server/polar/index.d.ts +29 -0
- package/dist/server/polar/index.js +49 -0
- package/dist/server/polar/webhook.d.ts +16 -0
- package/dist/server/polar/webhook.js +118 -0
- package/dist/site/config.d.ts +63 -0
- package/dist/site/config.js +48 -0
- package/dist/site/context.d.ts +11 -0
- package/dist/site/context.js +23 -0
- package/dist/site/hook.d.ts +15 -0
- package/dist/site/hook.js +38 -0
- package/dist/site/manifest.d.ts +6 -0
- package/dist/site/manifest.js +32 -0
- package/dist/site/og.d.ts +14 -0
- package/dist/site/og.js +116 -0
- package/dist/site/robots.d.ts +10 -0
- package/dist/site/robots.js +54 -0
- package/dist/site/schema.d.ts +67 -0
- package/dist/site/schema.js +67 -0
- package/dist/site/seo-helpers.d.ts +36 -0
- package/dist/site/seo-helpers.js +21 -0
- package/dist/site/sitemap-xsl.d.ts +6 -0
- package/dist/site/sitemap-xsl.js +164 -0
- package/dist/site/sitemap.d.ts +20 -0
- package/dist/site/sitemap.js +61 -0
- package/dist/styles/yaxa.css +84 -0
- package/dist/theme/theme.svelte.d.ts +74 -0
- package/dist/theme/theme.svelte.js +421 -0
- package/dist/utils/cn.d.ts +3 -0
- package/dist/utils/cn.js +3 -0
- package/dist/vite/index.d.ts +20 -0
- package/dist/vite/index.js +29 -0
- package/package.json +108 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Card from '../layout/Card.svelte';
|
|
3
|
+
import Button from '../elements/Button.svelte';
|
|
4
|
+
import Badge from '../elements/Badge.svelte';
|
|
5
|
+
import Icon from '../elements/Icon.svelte';
|
|
6
|
+
import type { PricingFeature } from '../../server/db/types';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
price: number;
|
|
12
|
+
interval?: 'month' | 'year';
|
|
13
|
+
yearlyDiscount?: number;
|
|
14
|
+
features: (string | PricingFeature)[];
|
|
15
|
+
productId?: string;
|
|
16
|
+
popular?: boolean;
|
|
17
|
+
badge?: string;
|
|
18
|
+
buttonText?: string;
|
|
19
|
+
loading?: boolean;
|
|
20
|
+
class?: string;
|
|
21
|
+
oncheckout?: (productId?: string) => void | Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
name,
|
|
26
|
+
description,
|
|
27
|
+
price,
|
|
28
|
+
interval = 'month',
|
|
29
|
+
yearlyDiscount = 0,
|
|
30
|
+
features = [],
|
|
31
|
+
productId,
|
|
32
|
+
popular = false,
|
|
33
|
+
badge,
|
|
34
|
+
buttonText,
|
|
35
|
+
loading = false,
|
|
36
|
+
class: className = '',
|
|
37
|
+
oncheckout
|
|
38
|
+
}: Props = $props();
|
|
39
|
+
|
|
40
|
+
let isSubmitting = $state(false);
|
|
41
|
+
|
|
42
|
+
async function handleCheckout() {
|
|
43
|
+
if (oncheckout) {
|
|
44
|
+
isSubmitting = true;
|
|
45
|
+
try {
|
|
46
|
+
await oncheckout(productId);
|
|
47
|
+
} finally {
|
|
48
|
+
isSubmitting = false;
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!productId) return;
|
|
54
|
+
|
|
55
|
+
isSubmitting = true;
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch('/api/checkout', {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { 'Content-Type': 'application/json' },
|
|
60
|
+
body: JSON.stringify({ productId })
|
|
61
|
+
});
|
|
62
|
+
const data = await res.json();
|
|
63
|
+
if (data.url && typeof window !== 'undefined') {
|
|
64
|
+
window.location.href = data.url;
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error('Checkout error:', err);
|
|
68
|
+
} finally {
|
|
69
|
+
isSubmitting = false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<Card
|
|
75
|
+
class="relative flex flex-col justify-between p-6 transition-all duration-200 sm:p-8 {popular
|
|
76
|
+
? 'border-2 border-primary-500 shadow-xl ring-1 ring-primary-500/20 dark:border-primary-500'
|
|
77
|
+
: 'border-zinc-200 shadow-md dark:border-zinc-800'} {className}"
|
|
78
|
+
>
|
|
79
|
+
<!-- Popular Banner / Badge -->
|
|
80
|
+
{#if popular || badge}
|
|
81
|
+
<div class="absolute -top-3 left-1/2 -translate-x-1/2">
|
|
82
|
+
<Badge
|
|
83
|
+
color="primary"
|
|
84
|
+
variant="solid"
|
|
85
|
+
size="sm"
|
|
86
|
+
class="font-semibold tracking-wider uppercase shadow-sm"
|
|
87
|
+
>
|
|
88
|
+
{badge || 'Most Popular'}
|
|
89
|
+
</Badge>
|
|
90
|
+
</div>
|
|
91
|
+
{/if}
|
|
92
|
+
|
|
93
|
+
<div class="space-y-6">
|
|
94
|
+
<!-- Tier Title & Description -->
|
|
95
|
+
<div class="space-y-2">
|
|
96
|
+
<h3 class="text-xl font-bold text-zinc-900 dark:text-zinc-100">{name}</h3>
|
|
97
|
+
<p class="min-h-[40px] text-sm leading-relaxed text-zinc-500 dark:text-zinc-400">
|
|
98
|
+
{description}
|
|
99
|
+
</p>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<!-- Price -->
|
|
103
|
+
<div class="flex items-baseline gap-1">
|
|
104
|
+
<span class="text-4xl font-extrabold tracking-tight text-zinc-900 dark:text-zinc-50">
|
|
105
|
+
${price}
|
|
106
|
+
</span>
|
|
107
|
+
<span class="text-sm font-medium text-zinc-500 dark:text-zinc-400">
|
|
108
|
+
/{interval === 'year' ? 'yr' : 'mo'}
|
|
109
|
+
</span>
|
|
110
|
+
{#if interval === 'year' && yearlyDiscount > 0}
|
|
111
|
+
<Badge color="success" variant="subtle" size="xs" class="ml-2">
|
|
112
|
+
Save {yearlyDiscount}%
|
|
113
|
+
</Badge>
|
|
114
|
+
{/if}
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<hr class="border-zinc-200 dark:border-zinc-800" />
|
|
118
|
+
|
|
119
|
+
<!-- Features List -->
|
|
120
|
+
<div class="space-y-3">
|
|
121
|
+
<span class="text-xs font-semibold tracking-wider text-zinc-400 uppercase dark:text-zinc-500">
|
|
122
|
+
Included features:
|
|
123
|
+
</span>
|
|
124
|
+
<ul class="space-y-2.5 text-sm">
|
|
125
|
+
{#each features as feat}
|
|
126
|
+
{@const isObj = typeof feat === 'object'}
|
|
127
|
+
{@const featName = isObj ? feat.name : feat}
|
|
128
|
+
{@const included = isObj ? feat.included !== false : true}
|
|
129
|
+
<li
|
|
130
|
+
class="flex items-center gap-2.5 {included
|
|
131
|
+
? 'text-zinc-700 dark:text-zinc-200'
|
|
132
|
+
: 'text-zinc-400 line-through dark:text-zinc-600'}"
|
|
133
|
+
>
|
|
134
|
+
{#if included}
|
|
135
|
+
<div
|
|
136
|
+
class="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-primary-50 text-primary-600 dark:bg-primary-950/40 dark:text-primary-400"
|
|
137
|
+
>
|
|
138
|
+
<Icon name="check" class="h-3.5 w-3.5" />
|
|
139
|
+
</div>
|
|
140
|
+
{:else}
|
|
141
|
+
<div
|
|
142
|
+
class="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-zinc-100 text-zinc-400 dark:bg-zinc-800 dark:text-zinc-600"
|
|
143
|
+
>
|
|
144
|
+
<Icon name="close" class="h-3.5 w-3.5" />
|
|
145
|
+
</div>
|
|
146
|
+
{/if}
|
|
147
|
+
<span>{featName}</span>
|
|
148
|
+
</li>
|
|
149
|
+
{/each}
|
|
150
|
+
</ul>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<!-- CTA Button -->
|
|
155
|
+
<div class="pt-8">
|
|
156
|
+
<Button
|
|
157
|
+
variant={popular ? 'solid' : 'outline'}
|
|
158
|
+
color={popular ? 'primary' : 'neutral'}
|
|
159
|
+
class="w-full justify-center py-2.5 font-semibold shadow-xs"
|
|
160
|
+
loading={loading || isSubmitting}
|
|
161
|
+
onclick={handleCheckout}
|
|
162
|
+
>
|
|
163
|
+
{buttonText || (price === 0 ? 'Get Started Free' : 'Upgrade Plan')}
|
|
164
|
+
</Button>
|
|
165
|
+
</div>
|
|
166
|
+
</Card>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PricingFeature } from '../../server/db/types';
|
|
2
|
+
interface Props {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
price: number;
|
|
6
|
+
interval?: 'month' | 'year';
|
|
7
|
+
yearlyDiscount?: number;
|
|
8
|
+
features: (string | PricingFeature)[];
|
|
9
|
+
productId?: string;
|
|
10
|
+
popular?: boolean;
|
|
11
|
+
badge?: string;
|
|
12
|
+
buttonText?: string;
|
|
13
|
+
loading?: boolean;
|
|
14
|
+
class?: string;
|
|
15
|
+
oncheckout?: (productId?: string) => void | Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
declare const PricingCard: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type PricingCard = ReturnType<typeof PricingCard>;
|
|
19
|
+
export default PricingCard;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import PricingCard from './PricingCard.svelte';
|
|
3
|
+
import Badge from '../elements/Badge.svelte';
|
|
4
|
+
import type { PricingTier } from '../../server/db/types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
tiers?: PricingTier[];
|
|
8
|
+
yearlyDiscount?: number;
|
|
9
|
+
defaultInterval?: 'month' | 'year';
|
|
10
|
+
class?: string;
|
|
11
|
+
oncheckout?: (tier: PricingTier, interval: 'month' | 'year') => void | Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DEFAULT_TIERS: PricingTier[] = [
|
|
15
|
+
{
|
|
16
|
+
id: 'free',
|
|
17
|
+
name: 'Hobby',
|
|
18
|
+
description: 'Perfect for side projects and evaluating Yaxa.',
|
|
19
|
+
priceMonthly: 0,
|
|
20
|
+
priceYearly: 0,
|
|
21
|
+
popular: false,
|
|
22
|
+
buttonText: 'Get Started',
|
|
23
|
+
features: [
|
|
24
|
+
'Up to 3 projects',
|
|
25
|
+
'Community support',
|
|
26
|
+
'Basic analytics',
|
|
27
|
+
{ name: 'Custom domains', included: false },
|
|
28
|
+
{ name: 'Priority support', included: false }
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'pro',
|
|
33
|
+
name: 'Pro Solo',
|
|
34
|
+
description: 'Everything a solo indie hacker needs to launch & scale.',
|
|
35
|
+
priceMonthly: 29,
|
|
36
|
+
priceYearly: 24,
|
|
37
|
+
popular: true,
|
|
38
|
+
badge: 'Recommended',
|
|
39
|
+
buttonText: 'Start 14-Day Free Trial',
|
|
40
|
+
features: [
|
|
41
|
+
'Unlimited projects',
|
|
42
|
+
'Custom domains & SSL',
|
|
43
|
+
'Automated SEO suite',
|
|
44
|
+
'Polar.sh automated billing',
|
|
45
|
+
'Better-Auth multi-tenant',
|
|
46
|
+
'Email & Discord support'
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'team',
|
|
51
|
+
name: 'Agency & Scale',
|
|
52
|
+
description: 'Collaborate with unlimited team members and clients.',
|
|
53
|
+
priceMonthly: 79,
|
|
54
|
+
priceYearly: 65,
|
|
55
|
+
popular: false,
|
|
56
|
+
buttonText: 'Subscribe to Scale',
|
|
57
|
+
features: [
|
|
58
|
+
'Everything in Pro Solo',
|
|
59
|
+
'Unlimited team members',
|
|
60
|
+
'Multi-workspace switching',
|
|
61
|
+
'Dedicated database replica',
|
|
62
|
+
'99.9% uptime SLA',
|
|
63
|
+
'1-on-1 architecture call'
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
let {
|
|
69
|
+
tiers = DEFAULT_TIERS,
|
|
70
|
+
yearlyDiscount = 20,
|
|
71
|
+
defaultInterval = 'month',
|
|
72
|
+
class: className = '',
|
|
73
|
+
oncheckout
|
|
74
|
+
}: Props = $props();
|
|
75
|
+
|
|
76
|
+
let customInterval = $state<'month' | 'year' | null>(null);
|
|
77
|
+
let interval = $derived(customInterval ?? defaultInterval);
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<div class="space-y-10 {className}">
|
|
81
|
+
<!-- Billing Cycle Toggle -->
|
|
82
|
+
<div class="flex flex-col items-center justify-center gap-3 text-center">
|
|
83
|
+
<div
|
|
84
|
+
class="inline-flex items-center rounded-full border border-zinc-200 bg-zinc-100 p-1 dark:border-zinc-700 dark:bg-zinc-800/80"
|
|
85
|
+
>
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
class="rounded-full px-4 py-1.5 text-xs font-semibold transition-all {interval === 'month'
|
|
89
|
+
? 'bg-white text-zinc-900 shadow-xs dark:bg-zinc-900 dark:text-zinc-50'
|
|
90
|
+
: 'text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-200'}"
|
|
91
|
+
onclick={() => {
|
|
92
|
+
customInterval = 'month';
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
Monthly billing
|
|
96
|
+
</button>
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
class="flex items-center gap-1.5 rounded-full px-4 py-1.5 text-xs font-semibold transition-all {interval ===
|
|
100
|
+
'year'
|
|
101
|
+
? 'bg-white text-zinc-900 shadow-xs dark:bg-zinc-900 dark:text-zinc-50'
|
|
102
|
+
: 'text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-200'}"
|
|
103
|
+
onclick={() => {
|
|
104
|
+
customInterval = 'year';
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
<span>Annual billing</span>
|
|
108
|
+
{#if yearlyDiscount > 0}
|
|
109
|
+
<Badge color="success" variant="solid" size="xs" class="px-1.5 py-0.5 text-[10px]">
|
|
110
|
+
-{yearlyDiscount}%
|
|
111
|
+
</Badge>
|
|
112
|
+
{/if}
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<!-- Pricing Grid -->
|
|
118
|
+
<div class="grid grid-cols-1 items-stretch gap-8 md:grid-cols-2 lg:grid-cols-3">
|
|
119
|
+
{#each tiers as tier}
|
|
120
|
+
{@const currentPrice = interval === 'year' ? tier.priceYearly : tier.priceMonthly}
|
|
121
|
+
{@const currentProductId = interval === 'year' ? tier.productIdYearly : tier.productIdMonthly}
|
|
122
|
+
<PricingCard
|
|
123
|
+
name={tier.name}
|
|
124
|
+
description={tier.description}
|
|
125
|
+
price={currentPrice}
|
|
126
|
+
{interval}
|
|
127
|
+
{yearlyDiscount}
|
|
128
|
+
features={tier.features}
|
|
129
|
+
popular={tier.popular}
|
|
130
|
+
badge={tier.badge}
|
|
131
|
+
buttonText={tier.buttonText}
|
|
132
|
+
productId={currentProductId}
|
|
133
|
+
oncheckout={() => {
|
|
134
|
+
if (oncheckout) return oncheckout(tier, interval);
|
|
135
|
+
}}
|
|
136
|
+
/>
|
|
137
|
+
{/each}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PricingTier } from '../../server/db/types';
|
|
2
|
+
interface Props {
|
|
3
|
+
tiers?: PricingTier[];
|
|
4
|
+
yearlyDiscount?: number;
|
|
5
|
+
defaultInterval?: 'month' | 'year';
|
|
6
|
+
class?: string;
|
|
7
|
+
oncheckout?: (tier: PricingTier, interval: 'month' | 'year') => void | Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
declare const PricingTable: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type PricingTable = ReturnType<typeof PricingTable>;
|
|
11
|
+
export default PricingTable;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Card from '../layout/Card.svelte';
|
|
3
|
+
import Badge from '../elements/Badge.svelte';
|
|
4
|
+
import Button from '../elements/Button.svelte';
|
|
5
|
+
import Icon from '../elements/Icon.svelte';
|
|
6
|
+
import type { SubscriptionStatus } from '../../server/db/types';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
tierName?: string;
|
|
10
|
+
status?: SubscriptionStatus;
|
|
11
|
+
interval?: 'month' | 'year';
|
|
12
|
+
amount?: number;
|
|
13
|
+
currentPeriodEnd?: Date | string | null;
|
|
14
|
+
cancelAtPeriodEnd?: boolean;
|
|
15
|
+
customerId?: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
onmanage?: () => void | Promise<void>;
|
|
18
|
+
onupgrade?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
tierName = 'Pro Plan',
|
|
23
|
+
status = 'active',
|
|
24
|
+
interval = 'month',
|
|
25
|
+
amount = 29,
|
|
26
|
+
currentPeriodEnd = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
|
|
27
|
+
cancelAtPeriodEnd = false,
|
|
28
|
+
customerId,
|
|
29
|
+
class: className = '',
|
|
30
|
+
onmanage,
|
|
31
|
+
onupgrade
|
|
32
|
+
}: Props = $props();
|
|
33
|
+
|
|
34
|
+
let isOpeningPortal = $state(false);
|
|
35
|
+
|
|
36
|
+
const formattedDate = $derived.by(() => {
|
|
37
|
+
if (!currentPeriodEnd) return null;
|
|
38
|
+
const d = typeof currentPeriodEnd === 'string' ? new Date(currentPeriodEnd) : currentPeriodEnd;
|
|
39
|
+
return d.toLocaleDateString(undefined, {
|
|
40
|
+
year: 'numeric',
|
|
41
|
+
month: 'short',
|
|
42
|
+
day: 'numeric'
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const statusColor = $derived.by(() => {
|
|
47
|
+
switch (status) {
|
|
48
|
+
case 'active':
|
|
49
|
+
return 'success';
|
|
50
|
+
case 'trialing':
|
|
51
|
+
return 'info';
|
|
52
|
+
case 'past_due':
|
|
53
|
+
case 'unpaid':
|
|
54
|
+
return 'warning';
|
|
55
|
+
case 'canceled':
|
|
56
|
+
default:
|
|
57
|
+
return 'neutral';
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
async function handleManageBilling() {
|
|
62
|
+
if (onmanage) {
|
|
63
|
+
isOpeningPortal = true;
|
|
64
|
+
try {
|
|
65
|
+
await onmanage();
|
|
66
|
+
} finally {
|
|
67
|
+
isOpeningPortal = false;
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!customerId) return;
|
|
73
|
+
|
|
74
|
+
isOpeningPortal = true;
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetch('/api/billing/portal', {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'Content-Type': 'application/json' },
|
|
79
|
+
body: JSON.stringify({ customerId })
|
|
80
|
+
});
|
|
81
|
+
const data = await res.json();
|
|
82
|
+
if (data.url && typeof window !== 'undefined') {
|
|
83
|
+
window.location.href = data.url;
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error('Portal error:', err);
|
|
87
|
+
} finally {
|
|
88
|
+
isOpeningPortal = false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
</script>
|
|
92
|
+
|
|
93
|
+
<Card class="space-y-6 border-zinc-200 p-6 shadow-md sm:p-7 dark:border-zinc-800 {className}">
|
|
94
|
+
<div class="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
|
|
95
|
+
<div class="space-y-1">
|
|
96
|
+
<div class="flex items-center gap-3">
|
|
97
|
+
<h3 class="text-xl font-bold text-zinc-900 dark:text-zinc-100">{tierName}</h3>
|
|
98
|
+
<Badge color={statusColor} variant="subtle" size="sm" class="font-semibold uppercase">
|
|
99
|
+
{status}
|
|
100
|
+
</Badge>
|
|
101
|
+
</div>
|
|
102
|
+
<p class="text-sm text-zinc-500 dark:text-zinc-400">
|
|
103
|
+
${amount}/{interval === 'year' ? 'yr' : 'mo'} · Billed {interval === 'year'
|
|
104
|
+
? 'annually'
|
|
105
|
+
: 'monthly'}
|
|
106
|
+
</p>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div class="flex items-center gap-2">
|
|
110
|
+
{#if onupgrade}
|
|
111
|
+
<Button variant="outline" size="sm" onclick={onupgrade}>Change Plan</Button>
|
|
112
|
+
{/if}
|
|
113
|
+
<Button
|
|
114
|
+
variant="solid"
|
|
115
|
+
color="primary"
|
|
116
|
+
size="sm"
|
|
117
|
+
class="gap-1.5 font-medium"
|
|
118
|
+
loading={isOpeningPortal}
|
|
119
|
+
onclick={handleManageBilling}
|
|
120
|
+
>
|
|
121
|
+
<Icon name="credit-card" class="h-3.5 w-3.5" />
|
|
122
|
+
<span>Manage Subscription</span>
|
|
123
|
+
</Button>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<hr class="border-zinc-200 dark:border-zinc-800" />
|
|
128
|
+
|
|
129
|
+
<div class="grid grid-cols-1 gap-4 text-xs sm:grid-cols-3">
|
|
130
|
+
<div class="space-y-1">
|
|
131
|
+
<span class="font-medium text-zinc-400 dark:text-zinc-500">Renewal Date</span>
|
|
132
|
+
<p class="font-semibold text-zinc-800 dark:text-zinc-200">
|
|
133
|
+
{#if cancelAtPeriodEnd}
|
|
134
|
+
Expires on {formattedDate}
|
|
135
|
+
{:else if formattedDate}
|
|
136
|
+
Renews on {formattedDate}
|
|
137
|
+
{:else}
|
|
138
|
+
N/A
|
|
139
|
+
{/if}
|
|
140
|
+
</p>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div class="space-y-1">
|
|
144
|
+
<span class="font-medium text-zinc-400 dark:text-zinc-500">Payment Provider</span>
|
|
145
|
+
<p class="flex items-center gap-1.5 font-semibold text-zinc-800 dark:text-zinc-200">
|
|
146
|
+
<span class="inline-block h-2 w-2 rounded-full bg-emerald-500"></span>
|
|
147
|
+
Polar.sh Merchant of Record
|
|
148
|
+
</p>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div class="space-y-1">
|
|
152
|
+
<span class="font-medium text-zinc-400 dark:text-zinc-500">Auto-Renew</span>
|
|
153
|
+
<p
|
|
154
|
+
class="font-semibold {cancelAtPeriodEnd
|
|
155
|
+
? 'text-amber-600 dark:text-amber-400'
|
|
156
|
+
: 'text-emerald-600 dark:text-emerald-400'}"
|
|
157
|
+
>
|
|
158
|
+
{cancelAtPeriodEnd ? 'Disabled (Cancels at end of cycle)' : 'Active (Automatic renewal)'}
|
|
159
|
+
</p>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</Card>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SubscriptionStatus } from '../../server/db/types';
|
|
2
|
+
interface Props {
|
|
3
|
+
tierName?: string;
|
|
4
|
+
status?: SubscriptionStatus;
|
|
5
|
+
interval?: 'month' | 'year';
|
|
6
|
+
amount?: number;
|
|
7
|
+
currentPeriodEnd?: Date | string | null;
|
|
8
|
+
cancelAtPeriodEnd?: boolean;
|
|
9
|
+
customerId?: string;
|
|
10
|
+
class?: string;
|
|
11
|
+
onmanage?: () => void | Promise<void>;
|
|
12
|
+
onupgrade?: () => void;
|
|
13
|
+
}
|
|
14
|
+
declare const SubscriptionCard: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type SubscriptionCard = ReturnType<typeof SubscriptionCard>;
|
|
16
|
+
export default SubscriptionCard;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Avatar from '../elements/Avatar.svelte';
|
|
3
|
+
import Badge from '../elements/Badge.svelte';
|
|
4
|
+
import Icon from '../elements/Icon.svelte';
|
|
5
|
+
import DropdownMenu, { type MenuItem } from '../navigation/DropdownMenu.svelte';
|
|
6
|
+
import { useAuth } from '../../composables/useAuth.svelte';
|
|
7
|
+
|
|
8
|
+
interface UserInfo {
|
|
9
|
+
name?: string | null;
|
|
10
|
+
email?: string | null;
|
|
11
|
+
image?: string | null;
|
|
12
|
+
role?: string | null;
|
|
13
|
+
tier?: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
user?: UserInfo | null;
|
|
18
|
+
tier?: string;
|
|
19
|
+
dashboardUrl?: string;
|
|
20
|
+
settingsUrl?: string;
|
|
21
|
+
billingUrl?: string;
|
|
22
|
+
class?: string;
|
|
23
|
+
onsignout?: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
user: propUser,
|
|
28
|
+
tier = 'Free',
|
|
29
|
+
dashboardUrl = '/dashboard',
|
|
30
|
+
settingsUrl = '/settings',
|
|
31
|
+
billingUrl = '/billing',
|
|
32
|
+
class: className = '',
|
|
33
|
+
onsignout
|
|
34
|
+
}: Props = $props();
|
|
35
|
+
|
|
36
|
+
const auth = useAuth();
|
|
37
|
+
|
|
38
|
+
const currentUser = $derived(
|
|
39
|
+
propUser ||
|
|
40
|
+
auth.user || {
|
|
41
|
+
name: 'Solo Developer',
|
|
42
|
+
email: 'dev@yaxa.dev',
|
|
43
|
+
image: null,
|
|
44
|
+
tier
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const initials = $derived.by(() => {
|
|
49
|
+
const name = currentUser?.name || currentUser?.email || 'U';
|
|
50
|
+
return name
|
|
51
|
+
.split(' ')
|
|
52
|
+
.map((n: string) => n[0])
|
|
53
|
+
.join('')
|
|
54
|
+
.toUpperCase()
|
|
55
|
+
.slice(0, 2);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const menuItems: MenuItem[] = $derived([
|
|
59
|
+
{
|
|
60
|
+
id: 'dashboard',
|
|
61
|
+
label: 'Dashboard',
|
|
62
|
+
icon: 'home',
|
|
63
|
+
onSelect: () => {
|
|
64
|
+
if (typeof window !== 'undefined') window.location.href = dashboardUrl;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'settings',
|
|
69
|
+
label: 'Settings',
|
|
70
|
+
icon: 'settings',
|
|
71
|
+
onSelect: () => {
|
|
72
|
+
if (typeof window !== 'undefined') window.location.href = settingsUrl;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'billing',
|
|
77
|
+
label: 'Billing & Plan',
|
|
78
|
+
icon: 'credit-card',
|
|
79
|
+
onSelect: () => {
|
|
80
|
+
if (typeof window !== 'undefined') window.location.href = billingUrl;
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'sep',
|
|
85
|
+
label: '',
|
|
86
|
+
separator: true
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'logout',
|
|
90
|
+
label: 'Sign Out',
|
|
91
|
+
icon: 'log-out',
|
|
92
|
+
destructive: true,
|
|
93
|
+
onSelect: async () => {
|
|
94
|
+
await auth.signOut();
|
|
95
|
+
if (onsignout) onsignout();
|
|
96
|
+
if (typeof window !== 'undefined') window.location.href = '/login';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
]);
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
{#snippet customTrigger()}
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
class="group flex items-center gap-3 rounded-full p-1 transition-all hover:bg-zinc-100 focus:outline-hidden focus-visible:ring-2 focus-visible:ring-primary-500 dark:hover:bg-zinc-800"
|
|
106
|
+
aria-label="User navigation menu"
|
|
107
|
+
>
|
|
108
|
+
<Avatar
|
|
109
|
+
src={currentUser?.image || undefined}
|
|
110
|
+
alt={currentUser?.name || 'User'}
|
|
111
|
+
fallback={initials}
|
|
112
|
+
size="sm"
|
|
113
|
+
class="border border-zinc-200 shadow-xs dark:border-zinc-700"
|
|
114
|
+
/>
|
|
115
|
+
<div class="hidden text-left sm:block">
|
|
116
|
+
<div class="flex items-center gap-2">
|
|
117
|
+
<span class="text-xs font-semibold text-zinc-900 dark:text-zinc-100">
|
|
118
|
+
{currentUser?.name || 'User'}
|
|
119
|
+
</span>
|
|
120
|
+
<Badge color="primary" variant="subtle" size="xs">
|
|
121
|
+
{tier.toUpperCase()}
|
|
122
|
+
</Badge>
|
|
123
|
+
</div>
|
|
124
|
+
<span class="block max-w-[140px] truncate text-[11px] text-zinc-500 dark:text-zinc-400">
|
|
125
|
+
{currentUser?.email || ''}
|
|
126
|
+
</span>
|
|
127
|
+
</div>
|
|
128
|
+
<Icon
|
|
129
|
+
name="chevron-down"
|
|
130
|
+
class="hidden h-3.5 w-3.5 text-zinc-400 transition-transform group-hover:text-zinc-600 sm:block dark:group-hover:text-zinc-200"
|
|
131
|
+
/>
|
|
132
|
+
</button>
|
|
133
|
+
{/snippet}
|
|
134
|
+
|
|
135
|
+
<div class="relative inline-flex items-center {className}">
|
|
136
|
+
<DropdownMenu items={menuItems} trigger={customTrigger} />
|
|
137
|
+
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface UserInfo {
|
|
2
|
+
name?: string | null;
|
|
3
|
+
email?: string | null;
|
|
4
|
+
image?: string | null;
|
|
5
|
+
role?: string | null;
|
|
6
|
+
tier?: string | null;
|
|
7
|
+
}
|
|
8
|
+
interface Props {
|
|
9
|
+
user?: UserInfo | null;
|
|
10
|
+
tier?: string;
|
|
11
|
+
dashboardUrl?: string;
|
|
12
|
+
settingsUrl?: string;
|
|
13
|
+
billingUrl?: string;
|
|
14
|
+
class?: string;
|
|
15
|
+
onsignout?: () => void;
|
|
16
|
+
}
|
|
17
|
+
declare const UserMenu: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type UserMenu = ReturnType<typeof UserMenu>;
|
|
19
|
+
export default UserMenu;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { SiteConfig } from '../../site/config';
|
|
3
|
+
import { getSiteConfig } from '../../site/context';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
config?: SiteConfig;
|
|
7
|
+
faviconSvg?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let { config, faviconSvg = '/favicon.svg' }: Props = $props();
|
|
11
|
+
|
|
12
|
+
let currentConfig = $derived(config || getSiteConfig());
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<svelte:head>
|
|
16
|
+
<link rel="icon" type="image/svg+xml" href={faviconSvg} />
|
|
17
|
+
<link rel="manifest" href="/site.webmanifest" />
|
|
18
|
+
<meta name="theme-color" content={currentConfig.theme?.primaryColor || '#ff3e00'} />
|
|
19
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
20
|
+
<meta name="apple-mobile-web-app-title" content={currentConfig.name} />
|
|
21
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
|
22
|
+
</svelte:head>
|