Skip to content
Draft
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
14 changes: 12 additions & 2 deletions app/components/PricingPlanCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<h3 :class="compact ? 'text-lg' : 'text-xl'" class="font-bold">{{ plan.name }}</h3>
<div :class="compact ? 'mt-2' : 'mt-4'" class="flex items-baseline gap-1">
<span :class="compact ? 'text-2xl' : 'text-4xl'" class="font-extrabold">
{{ formatPrice(plan.monthlyPrice) }}
{{ formatPriceLocalized(plan.monthlyPrice, currencyContext) }}
</span>
<span class="text-sm text-gray-500 dark:text-gray-400"> / month </span>
</div>
Expand Down Expand Up @@ -70,7 +70,7 @@
</template>

<script setup lang="ts">
import { formatPrice, type PricingPlan } from '~/utils/pricing'
import { formatPriceLocalized, type PricingPlan } from '~/utils/pricing'

interface Props {
plan: PricingPlan
Expand All @@ -90,8 +90,18 @@
}>()

const userStore = useUserStore()
const { data } = useAuth()
const config = useRuntimeConfig()
const subscriptionsEnabled = computed(() => config.public.subscriptionsEnabled)
const { timezone } = useFormat()

const currencyContext = computed(() => ({
country: userStore.profile?.country,
preferredCurrency: userStore.profile?.currencyPreference,
profileTimezone: (data.value?.user as any)?.timezone || timezone.value,
browserTimezone: import.meta.client ? Intl.DateTimeFormat().resolvedOptions().timeZone : null,
locale: import.meta.client ? navigator.language : null
}))

const isCurrentPlan = computed(() => {
const currentTier = userStore.user?.subscriptionTier?.toLowerCase()
Expand Down
31 changes: 28 additions & 3 deletions app/components/landing/PricingPlans.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@

<div class="mt-4 flex items-baseline gap-1">
<span class="text-4xl font-extrabold">{{
formatPrice(getPrice(plan, billingInterval))
formatPriceLocalized(getPrice(plan, billingInterval), currencyContext)
}}</span>

<span class="text-sm text-gray-500 dark:text-gray-400">
Expand Down Expand Up @@ -128,23 +128,48 @@
import {
PRICING_PLANS,
calculateAnnualSavings,
formatPrice,
formatPriceLocalized,
getPrice,
getStripePriceId,
type BillingInterval,
type PricingPlan
} from '~/utils/pricing'

const { status } = useAuth()
const { status, data } = useAuth()
const userStore = useUserStore()
const { createCheckoutSession, openCustomerPortal } = useStripe()
const config = useRuntimeConfig()
const { timezone } = useFormat()

const billingInterval = ref<BillingInterval>('annual')
const loading = ref(false)
const selectedPlan = ref<string | null>(null)
const subscriptionsEnabled = computed(() => config.public.subscriptionsEnabled)

const browserTimezone = computed(() => {
if (!import.meta.client) return null
return Intl.DateTimeFormat().resolvedOptions().timeZone
})

const browserLocale = computed(() => {
if (!import.meta.client) return null
return navigator.language
})

const currencyContext = computed(() => ({
country: userStore.profile?.country,
preferredCurrency: userStore.profile?.currencyPreference,
profileTimezone: (data.value?.user as any)?.timezone || timezone.value,
browserTimezone: browserTimezone.value,
locale: browserLocale.value
}))

onMounted(() => {
if (status.value === 'authenticated' && !userStore.profile) {
userStore.fetchProfile()
}
})

function isCurrentPlan(plan: PricingPlan): boolean {
if (!userStore.user || status.value !== 'authenticated') return false
const currentTier = userStore.user.subscriptionTier?.toLowerCase()
Expand Down
65 changes: 65 additions & 0 deletions app/components/profile/BasicSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,43 @@
}}</span>
</p>
</div>
<!-- Currency -->
<div class="group relative">
<div class="flex items-center gap-1 mb-1">
<label class="text-sm text-muted">Currency</label>
<button
class="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 rounded hover:bg-gray-100 dark:hover:bg-gray-800"
@click="startEdit('currencyPreference')"
>
<UIcon name="i-heroicons-pencil" class="w-3 h-3 text-muted hover:text-primary" />
</button>
</div>
<div v-if="editingField === 'currencyPreference'" class="flex gap-2 w-full relative z-50">
<USelectMenu
v-model="currencyModel"
:items="currencyOptions"
option-attribute="label"
:search-attributes="['label', 'code']"
size="sm"
class="flex-1"
searchable
searchable-placeholder="Search currency..."
autofocus
:ui="{ content: 'w-full min-w-[var(--reka-popper-anchor-width)]' }"
@update:model-value="saveField"
/>
</div>
<div v-else class="space-y-1">
<p class="font-medium text-lg">
{{
modelValue.currencyPreference
? getCurrencyLabel(modelValue.currencyPreference)
: 'Auto (Based on location)'
}}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">In use: {{ currentCurrencyLabel }}</p>
</div>
</div>
<!-- Timezone -->
<div class="lg:col-span-2 group relative">
<div class="flex items-center gap-1 mb-1">
Expand Down Expand Up @@ -542,6 +579,7 @@

<script setup lang="ts">
import { countries } from '~/utils/countries'
import { ACCEPTED_CURRENCIES, getCurrencyLabel, resolveCurrencyContext } from '~/utils/currency'
const props = defineProps<{
modelValue: any
email: string
Expand All @@ -564,6 +602,33 @@
}
})

const currencyOptions = computed(() => [
{ code: '', label: 'Auto (Based on location)' },
...ACCEPTED_CURRENCIES
])

const currencyModel = computed({
get: () => {
const current = editValue.value ? editValue.value.toString() : ''
return currencyOptions.value.find((c) => c.code === current)
},
set: (val: any) => {
editValue.value = val?.code || ''
}
})

const currentCurrencyLabel = computed(() => {
const resolved = resolveCurrencyContext({
preferredCurrency: props.modelValue.currencyPreference,
country: props.modelValue.country,
profileTimezone: props.modelValue.timezone,
browserTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
locale: typeof navigator !== 'undefined' ? navigator.language : null
})

return `${resolved.currency} - ${getCurrencyLabel(resolved.currency)}`
})

const countriesWithLabel = computed(() =>
countries.map((c) => ({
...c,
Expand Down
3 changes: 2 additions & 1 deletion app/pages/profile/settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@
city: '',
state: '',
country: '',
timezone: ''
timezone: '',
currencyPreference: null
})

const sportSettings = ref<any[]>([])
Expand Down
12 changes: 12 additions & 0 deletions app/plugins/currency.client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { setCurrencyRates } from '~/utils/currency'

export default defineNuxtPlugin(async () => {
try {
const data = await $fetch<{ rates: Record<string, number> }>('/api/currency/rates')
if (data?.rates) {
setCurrencyRates(data.rates)
}
} catch (error) {
console.warn('[CurrencyRates] Failed to load rates on client.', error)
}
})
Loading