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.
Files changed (169) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +163 -0
  3. package/dist/assets/favicon.svg +15 -0
  4. package/dist/components/elements/Avatar.svelte +106 -0
  5. package/dist/components/elements/Avatar.svelte.d.ts +40 -0
  6. package/dist/components/elements/AvatarGroup.svelte +18 -0
  7. package/dist/components/elements/AvatarGroup.svelte.d.ts +8 -0
  8. package/dist/components/elements/Badge.svelte +202 -0
  9. package/dist/components/elements/Badge.svelte.d.ts +75 -0
  10. package/dist/components/elements/Button.svelte +300 -0
  11. package/dist/components/elements/Button.svelte.d.ts +109 -0
  12. package/dist/components/elements/ButtonGroup.svelte +22 -0
  13. package/dist/components/elements/ButtonGroup.svelte.d.ts +9 -0
  14. package/dist/components/elements/Chip.svelte +117 -0
  15. package/dist/components/elements/Chip.svelte.d.ts +15 -0
  16. package/dist/components/elements/DataTable.svelte +222 -0
  17. package/dist/components/elements/DataTable.svelte.d.ts +43 -0
  18. package/dist/components/elements/Icon.svelte +87 -0
  19. package/dist/components/elements/Icon.svelte.d.ts +10 -0
  20. package/dist/components/elements/Kbd.svelte +37 -0
  21. package/dist/components/elements/Kbd.svelte.d.ts +32 -0
  22. package/dist/components/elements/Link.svelte +63 -0
  23. package/dist/components/elements/Link.svelte.d.ts +15 -0
  24. package/dist/components/elements/Logo.svelte +77 -0
  25. package/dist/components/elements/Logo.svelte.d.ts +9 -0
  26. package/dist/components/elements/Meter.svelte +87 -0
  27. package/dist/components/elements/Meter.svelte.d.ts +14 -0
  28. package/dist/components/elements/Progress.svelte +63 -0
  29. package/dist/components/elements/Progress.svelte.d.ts +49 -0
  30. package/dist/components/elements/Skeleton.svelte +12 -0
  31. package/dist/components/elements/Skeleton.svelte.d.ts +6 -0
  32. package/dist/components/elements/Spinner.svelte +39 -0
  33. package/dist/components/elements/Spinner.svelte.d.ts +7 -0
  34. package/dist/components/forms/Checkbox.svelte +77 -0
  35. package/dist/components/forms/Checkbox.svelte.d.ts +17 -0
  36. package/dist/components/forms/ColorPicker.svelte +142 -0
  37. package/dist/components/forms/ColorPicker.svelte.d.ts +10 -0
  38. package/dist/components/forms/Form.svelte +84 -0
  39. package/dist/components/forms/Form.svelte.d.ts +14 -0
  40. package/dist/components/forms/FormField.svelte +56 -0
  41. package/dist/components/forms/FormField.svelte.d.ts +14 -0
  42. package/dist/components/forms/Input.svelte +138 -0
  43. package/dist/components/forms/Input.svelte.d.ts +58 -0
  44. package/dist/components/forms/RadioGroup.svelte +74 -0
  45. package/dist/components/forms/RadioGroup.svelte.d.ts +17 -0
  46. package/dist/components/forms/Select.svelte +91 -0
  47. package/dist/components/forms/Select.svelte.d.ts +50 -0
  48. package/dist/components/forms/Slider.svelte +37 -0
  49. package/dist/components/forms/Slider.svelte.d.ts +12 -0
  50. package/dist/components/forms/Switch.svelte +81 -0
  51. package/dist/components/forms/Switch.svelte.d.ts +53 -0
  52. package/dist/components/forms/Textarea.svelte +73 -0
  53. package/dist/components/forms/Textarea.svelte.d.ts +64 -0
  54. package/dist/components/layout/Card.svelte +89 -0
  55. package/dist/components/layout/Card.svelte.d.ts +64 -0
  56. package/dist/components/layout/Container.svelte +36 -0
  57. package/dist/components/layout/Container.svelte.d.ts +37 -0
  58. package/dist/components/layout/Divider.svelte +45 -0
  59. package/dist/components/layout/Divider.svelte.d.ts +11 -0
  60. package/dist/components/layout/Footer.svelte +96 -0
  61. package/dist/components/layout/Footer.svelte.d.ts +10 -0
  62. package/dist/components/layout/Header.svelte +175 -0
  63. package/dist/components/layout/Header.svelte.d.ts +12 -0
  64. package/dist/components/layout/Section.svelte +62 -0
  65. package/dist/components/layout/Section.svelte.d.ts +14 -0
  66. package/dist/components/layout/YaxaApp.svelte +48 -0
  67. package/dist/components/layout/YaxaApp.svelte.d.ts +12 -0
  68. package/dist/components/navigation/Breadcrumb.svelte +58 -0
  69. package/dist/components/navigation/Breadcrumb.svelte.d.ts +13 -0
  70. package/dist/components/navigation/CommandPalette.svelte +201 -0
  71. package/dist/components/navigation/CommandPalette.svelte.d.ts +19 -0
  72. package/dist/components/navigation/DropdownMenu.svelte +75 -0
  73. package/dist/components/navigation/DropdownMenu.svelte.d.ts +20 -0
  74. package/dist/components/navigation/Pagination.svelte +106 -0
  75. package/dist/components/navigation/Pagination.svelte.d.ts +10 -0
  76. package/dist/components/navigation/Tabs.svelte +91 -0
  77. package/dist/components/navigation/Tabs.svelte.d.ts +18 -0
  78. package/dist/components/overlays/Accordion.svelte +54 -0
  79. package/dist/components/overlays/Accordion.svelte.d.ts +16 -0
  80. package/dist/components/overlays/Alert.svelte +110 -0
  81. package/dist/components/overlays/Alert.svelte.d.ts +37 -0
  82. package/dist/components/overlays/ContextMenu.svelte +67 -0
  83. package/dist/components/overlays/ContextMenu.svelte.d.ts +21 -0
  84. package/dist/components/overlays/Modal.svelte +99 -0
  85. package/dist/components/overlays/Modal.svelte.d.ts +40 -0
  86. package/dist/components/overlays/Popover.svelte +33 -0
  87. package/dist/components/overlays/Popover.svelte.d.ts +11 -0
  88. package/dist/components/overlays/Slideover.svelte +102 -0
  89. package/dist/components/overlays/Slideover.svelte.d.ts +37 -0
  90. package/dist/components/overlays/Tooltip.svelte +39 -0
  91. package/dist/components/overlays/Tooltip.svelte.d.ts +12 -0
  92. package/dist/components/saas/AuthCard.svelte +273 -0
  93. package/dist/components/saas/AuthCard.svelte.d.ts +16 -0
  94. package/dist/components/saas/PricingCard.svelte +166 -0
  95. package/dist/components/saas/PricingCard.svelte.d.ts +19 -0
  96. package/dist/components/saas/PricingTable.svelte +139 -0
  97. package/dist/components/saas/PricingTable.svelte.d.ts +11 -0
  98. package/dist/components/saas/SubscriptionCard.svelte +162 -0
  99. package/dist/components/saas/SubscriptionCard.svelte.d.ts +16 -0
  100. package/dist/components/saas/UserMenu.svelte +137 -0
  101. package/dist/components/saas/UserMenu.svelte.d.ts +19 -0
  102. package/dist/components/seo/Favicons.svelte +22 -0
  103. package/dist/components/seo/Favicons.svelte.d.ts +8 -0
  104. package/dist/components/seo/OgImage.svelte +61 -0
  105. package/dist/components/seo/OgImage.svelte.d.ts +12 -0
  106. package/dist/components/seo/Seo.svelte +170 -0
  107. package/dist/components/seo/Seo.svelte.d.ts +21 -0
  108. package/dist/composables/useAuth.svelte.d.ts +23 -0
  109. package/dist/composables/useAuth.svelte.js +157 -0
  110. package/dist/composables/useClipboard.svelte.d.ts +9 -0
  111. package/dist/composables/useClipboard.svelte.js +36 -0
  112. package/dist/composables/useColorMode.svelte.d.ts +8 -0
  113. package/dist/composables/useColorMode.svelte.js +20 -0
  114. package/dist/composables/useDebounce.svelte.d.ts +3 -0
  115. package/dist/composables/useDebounce.svelte.js +17 -0
  116. package/dist/composables/useMediaQuery.svelte.d.ts +3 -0
  117. package/dist/composables/useMediaQuery.svelte.js +17 -0
  118. package/dist/composables/useShortcuts.svelte.d.ts +10 -0
  119. package/dist/composables/useShortcuts.svelte.js +50 -0
  120. package/dist/composables/useToast.d.ts +19 -0
  121. package/dist/composables/useToast.js +34 -0
  122. package/dist/index.d.ts +101 -0
  123. package/dist/index.js +92 -0
  124. package/dist/server/auth/hook.d.ts +14 -0
  125. package/dist/server/auth/hook.js +36 -0
  126. package/dist/server/auth/index.d.ts +39 -0
  127. package/dist/server/auth/index.js +69 -0
  128. package/dist/server/db/index.d.ts +18 -0
  129. package/dist/server/db/index.js +43 -0
  130. package/dist/server/db/schema-pg.d.ts +977 -0
  131. package/dist/server/db/schema-pg.js +83 -0
  132. package/dist/server/db/schema-sqlite.d.ts +1045 -0
  133. package/dist/server/db/schema-sqlite.js +83 -0
  134. package/dist/server/db/types.d.ts +44 -0
  135. package/dist/server/db/types.js +1 -0
  136. package/dist/server/email/index.d.ts +27 -0
  137. package/dist/server/email/index.js +79 -0
  138. package/dist/server/polar/index.d.ts +29 -0
  139. package/dist/server/polar/index.js +49 -0
  140. package/dist/server/polar/webhook.d.ts +16 -0
  141. package/dist/server/polar/webhook.js +118 -0
  142. package/dist/site/config.d.ts +63 -0
  143. package/dist/site/config.js +48 -0
  144. package/dist/site/context.d.ts +11 -0
  145. package/dist/site/context.js +23 -0
  146. package/dist/site/hook.d.ts +15 -0
  147. package/dist/site/hook.js +38 -0
  148. package/dist/site/manifest.d.ts +6 -0
  149. package/dist/site/manifest.js +32 -0
  150. package/dist/site/og.d.ts +14 -0
  151. package/dist/site/og.js +116 -0
  152. package/dist/site/robots.d.ts +10 -0
  153. package/dist/site/robots.js +54 -0
  154. package/dist/site/schema.d.ts +67 -0
  155. package/dist/site/schema.js +67 -0
  156. package/dist/site/seo-helpers.d.ts +36 -0
  157. package/dist/site/seo-helpers.js +21 -0
  158. package/dist/site/sitemap-xsl.d.ts +6 -0
  159. package/dist/site/sitemap-xsl.js +164 -0
  160. package/dist/site/sitemap.d.ts +20 -0
  161. package/dist/site/sitemap.js +61 -0
  162. package/dist/styles/yaxa.css +84 -0
  163. package/dist/theme/theme.svelte.d.ts +74 -0
  164. package/dist/theme/theme.svelte.js +421 -0
  165. package/dist/utils/cn.d.ts +3 -0
  166. package/dist/utils/cn.js +3 -0
  167. package/dist/vite/index.d.ts +20 -0
  168. package/dist/vite/index.js +29 -0
  169. 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>