mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
first draft migrating widget to vue3/vite
This commit is contained in:
@@ -283,7 +283,7 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
'script-src': ["{static}"] + (["http://localhost:5173", "ws://localhost:5173"] if settings.VITE_DEV_MODE else []),
|
||||
'object-src': ["'none'"],
|
||||
'frame-src': ['{static}'],
|
||||
'style-src': ["{static}", "{media}"],
|
||||
'style-src': ["{static}", "{media}"]+ (["unsafe-inline"] if settings.VITE_DEV_MODE else []),
|
||||
'connect-src': ["{dynamic}", "{media}"] + (["http://localhost:5173", "ws://localhost:5173"] if settings.VITE_DEV_MODE else []),
|
||||
'img-src': ["{static}", "{media}", "data:"] + img_src,
|
||||
'font-src': ["{static}"] + list(font_src),
|
||||
|
||||
2
src/pretix/static/pretixpresale/widget/TODOS.md
Normal file
2
src/pretix/static/pretixpresale/widget/TODOS.md
Normal file
@@ -0,0 +1,2 @@
|
||||
- modernize the sometimes native form submitting?
|
||||
- destructure props?
|
||||
15
src/pretix/static/pretixpresale/widget/index.html
Normal file
15
src/pretix/static/pretixpresale/widget/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Pretix Widget</title>
|
||||
<link rel="stylesheet" type="text/css" href="http://localhost:8000/testorg/testevent/widget/v2.css" crossorigin>
|
||||
</head>
|
||||
<body>
|
||||
<pretix-widget event="http://localhost:8000/testorg/testevent/"></pretix-widget>
|
||||
<!-- <script type="text/javascript" src="http://localhost:8000/widget/v2.en.js" async crossorigin></script> -->
|
||||
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
85
src/pretix/static/pretixpresale/widget/src/api.ts
Normal file
85
src/pretix/static/pretixpresale/widget/src/api.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { Category, DayEntry, EventEntry, MetaFilterField } from '~/types'
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number
|
||||
responseUrl: string
|
||||
|
||||
constructor (status: number, responseUrl: string) {
|
||||
super(`HTTP ${status}`)
|
||||
this.status = status
|
||||
this.responseUrl = responseUrl
|
||||
}
|
||||
}
|
||||
|
||||
// --- Product list ---
|
||||
|
||||
export interface ProductListResponse {
|
||||
target_url?: string
|
||||
subevent?: string | number
|
||||
name?: string
|
||||
frontpage_text?: string
|
||||
date_range?: string
|
||||
location?: string
|
||||
items_by_category?: Category[]
|
||||
currency?: string
|
||||
display_net_prices?: boolean
|
||||
voucher_explanation_text?: string
|
||||
error?: string
|
||||
display_add_to_cart?: boolean
|
||||
waiting_list_enabled?: boolean
|
||||
show_variations_expanded?: boolean
|
||||
cart_exists?: boolean
|
||||
vouchers_exist?: boolean
|
||||
has_seating_plan?: boolean
|
||||
has_seating_plan_waitinglist?: boolean
|
||||
itemnum?: number
|
||||
poweredby?: string
|
||||
events?: EventEntry[]
|
||||
has_more_events?: boolean
|
||||
meta_filter_fields?: MetaFilterField[]
|
||||
weeks?: DayEntry[][]
|
||||
date?: string
|
||||
days?: DayEntry[]
|
||||
week?: [number, number]
|
||||
}
|
||||
|
||||
export async function fetchProductList (url: string) {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
throw new ApiError(response.status, response.url)
|
||||
}
|
||||
return {
|
||||
data: await response.json() as ProductListResponse,
|
||||
responseUrl: response.url,
|
||||
}
|
||||
}
|
||||
|
||||
export interface CartResponse {
|
||||
redirect?: string
|
||||
cart_id?: string
|
||||
success?: boolean
|
||||
message?: string
|
||||
has_cart?: boolean
|
||||
async_id?: string
|
||||
check_url?: string
|
||||
}
|
||||
|
||||
export async function submitCart (endpoint: string, formData: FormData) {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams(formData as any).toString(),
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new ApiError(response.status, response.url)
|
||||
}
|
||||
return await response.json() as CartResponse
|
||||
}
|
||||
|
||||
export async function checkAsyncTask (url: string) {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
throw new ApiError(response.status, response.url)
|
||||
}
|
||||
return await response.json() as CartResponse
|
||||
}
|
||||
73
src/pretix/static/pretixpresale/widget/src/button.ts
Normal file
73
src/pretix/static/pretixpresale/widget/src/button.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { createApp, type App } from 'vue'
|
||||
import ButtonComponent from '~/components/Button.vue'
|
||||
import { createWidgetStore, StoreKey } from '~/sharedStore'
|
||||
import { makeid } from '~/utils'
|
||||
import type { WidgetData } from '~/types'
|
||||
|
||||
export function createButtonInstance (element: Element, htmlId?: string): App {
|
||||
let targetUrl = element.attributes.event.value
|
||||
if (!targetUrl.match(/\/$/)) {
|
||||
targetUrl += '/'
|
||||
}
|
||||
|
||||
const widgetData: WidgetData = JSON.parse(JSON.stringify(window.PretixWidget.widget_data))
|
||||
|
||||
for (const attr of Array.from(element.attributes)) {
|
||||
if (attr.name.match(/^data-.*$/)) {
|
||||
widgetData[attr.name.replace(/^data-/, '')] = attr.value
|
||||
}
|
||||
}
|
||||
|
||||
const rawItems = element.attributes.items?.value || ''
|
||||
|
||||
// Parse items string (format: "item_1=2,item_3=1")
|
||||
const buttonItems: { item: string; count: string }[] = []
|
||||
for (const itemStr of rawItems.split(',')) {
|
||||
if (itemStr.includes('=')) {
|
||||
const [item, count] = itemStr.split('=')
|
||||
buttonItems.push({ item, count })
|
||||
}
|
||||
}
|
||||
|
||||
const store = createWidgetStore({
|
||||
targetUrl,
|
||||
voucher: element.attributes.voucher?.value || null,
|
||||
subevent: element.attributes.subevent?.value || null,
|
||||
skipSsl: 'skip-ssl-check' in element.attributes,
|
||||
disableIframe: 'disable-iframe' in element.attributes,
|
||||
widgetData,
|
||||
htmlId: htmlId || element.id || makeid(16),
|
||||
isButton: true,
|
||||
buttonItems,
|
||||
buttonText: element.innerHTML
|
||||
})
|
||||
|
||||
const observer = new MutationObserver((mutationList) => {
|
||||
for (const mutation of mutationList) {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName?.startsWith('data-')) {
|
||||
const attrName = mutation.attributeName.substring(5)
|
||||
const attrValue = (mutation.target as Element).getAttribute(mutation.attributeName)
|
||||
if (attrValue !== null) {
|
||||
store.widgetData[attrName] = attrValue
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// TODO I don't think we need this anymore in vue3
|
||||
// if (element.tagName !== 'pretix-button') {
|
||||
// element.innerHTML = '<pretix-button>' + element.innerHTML + '</pretix-button>'
|
||||
// // Vue does not replace the container, so watch container as well
|
||||
// observer.observe(element, observerOptions)
|
||||
// }
|
||||
|
||||
const app = createApp(ButtonComponent)
|
||||
app.provide(StoreKey, store)
|
||||
app.config.errorHandler = (error, _vm, info) => {
|
||||
console.error('[pretix-button]', info, error)
|
||||
}
|
||||
app.mount(element)
|
||||
observer.observe(element, { attributes: true })
|
||||
|
||||
return app
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, inject, onMounted } from 'vue'
|
||||
import type { Item, Variation } from '~/types'
|
||||
import { StoreKey, globalWidgetId } from '~/sharedStore'
|
||||
import { STRINGS } from '~/i18n'
|
||||
|
||||
const props = defineProps<{
|
||||
item: Item
|
||||
variation?: Variation
|
||||
}>()
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
const quantity = ref<HTMLInputElement>()
|
||||
|
||||
const avail = computed(() => props.item.has_variations ? props.variation.avail : props.item.avail)
|
||||
|
||||
const orderMax = computed(() => props.item.has_variations ? props.variation.order_max : props.item.order_max)
|
||||
|
||||
const inputName = computed(() => {
|
||||
if (props.item.has_variations) {
|
||||
return `variation_${props.item.id}_${props.variation.id}`
|
||||
}
|
||||
return `item_${props.item.id}`
|
||||
})
|
||||
|
||||
const unavailabilityReasonMessage = computed(() => {
|
||||
const reason = props.item.current_unavailability_reason || props.variation?.current_unavailability_reason
|
||||
if (reason) {
|
||||
return STRINGS[`unavailable_${reason}`] || reason
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const voucherJumpLink = computed(() => `#${store.htmlId}-voucher-input`)
|
||||
|
||||
const ariaLabelledby = computed(() => `${store.htmlId}-item-label-${props.item.id}`)
|
||||
|
||||
const decLabel = computed(() => {
|
||||
// TODO
|
||||
const name = props.item.has_variations ? props.variation.value : props.item.name
|
||||
return `- ${name}: ${STRINGS.quantity_dec}`
|
||||
})
|
||||
|
||||
const incLabel = computed(() => {
|
||||
const name = props.item.has_variations ? props.variation.value : props.item.name
|
||||
return `+ ${name}: ${STRINGS.quantity_inc}`
|
||||
})
|
||||
|
||||
const labelSelectItem = computed(() => {
|
||||
if (props.item.has_variations) return STRINGS.select_variant.replace('%s', props.variation.value)
|
||||
return STRINGS.select_item.replace('%s', props.item.name)
|
||||
})
|
||||
|
||||
const waitingListShow = computed(() => avail.value[0] < 100 && store.waitingListEnabled && props.item.allow_waitinglist)
|
||||
|
||||
const waitingListUrl = computed(() => {
|
||||
let u = `${store.targetUrl}w/${globalWidgetId}/waitinglist/?locale=${LANG}&item=${props.item.id}`
|
||||
if (props.item.has_variations && props.variation) {
|
||||
u += `&var=${props.variation.id}`
|
||||
}
|
||||
if (store.subevent) {
|
||||
u += `&subevent=${store.subevent}`
|
||||
}
|
||||
const widgetDataJson = JSON.stringify(store.widgetData)
|
||||
u += `&widget_data=${encodeURIComponent(widgetDataJson)}`
|
||||
if (store.widgetData.consent) {
|
||||
u += `&consent=${encodeURIComponent(store.widgetData.consent)}`
|
||||
}
|
||||
return u
|
||||
})
|
||||
|
||||
function onStep (e: Event) {
|
||||
const target = e.target as HTMLElement
|
||||
const button = target.tagName === 'BUTTON' ? target : target.closest('button')
|
||||
if (!button || !quantity.value) return
|
||||
|
||||
const step = parseFloat(button.getAttribute('data-step') || '0')
|
||||
const input = quantity.value
|
||||
const min = parseFloat(input.min) || 0
|
||||
const max = parseFloat(input.max) || Number.MAX_SAFE_INTEGER
|
||||
const currentValue = parseInt(input.value || '0')
|
||||
input.value = String(Math.max(min, Math.min(max, currentValue + step)))
|
||||
input.dispatchEvent(new CustomEvent('change', { bubbles: true }))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Auto-select first item if single item with no variations
|
||||
if (
|
||||
store.itemnum === 1
|
||||
&& (!store.categories[0]?.items[0]?.has_variations || store.categories[0]?.items[0]?.variations.length < 2)
|
||||
&& !store.hasSeatingPlan
|
||||
&& quantity.value
|
||||
) {
|
||||
quantity.value.value = '1'
|
||||
if (orderMax.value === 1 && quantity.value.type === 'checkbox') {
|
||||
;(quantity.value as HTMLInputElement).checked = true
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<template lang="pug">
|
||||
.pretix-widget-availability-box
|
||||
.pretix-widget-availability-unavailable(v-if="item.current_unavailability_reason === 'require_voucher'")
|
||||
small
|
||||
a(:href="voucherJumpLink", :aria-describedby="ariaLabelledby") {{ unavailabilityReasonMessage }}
|
||||
.pretix-widget-availability-unavailable(v-else-if="unavailabilityReasonMessage")
|
||||
small {{ unavailabilityReasonMessage }}
|
||||
.pretix-widget-availability-unavailable(v-else-if="avail[0] < 100 && avail[0] > 10") {{ STRINGS.reserved }}
|
||||
.pretix-widget-availability-gone(v-else-if="avail[0] <= 10") {{ STRINGS.sold_out }}
|
||||
.pretix-widget-waiting-list-link(v-if="waitingListShow && !unavailabilityReasonMessage")
|
||||
a(:href="waitingListUrl", target="_blank", @click="$root.open_link_in_frame") {{ STRINGS.waiting_list }}
|
||||
.pretix-widget-availability-available(v-if="!unavailabilityReasonMessage && avail[0] === 100")
|
||||
label.pretix-widget-item-count-single-label.pretix-widget-btn-checkbox(v-if="orderMax === 1")
|
||||
input(
|
||||
ref="quantity",
|
||||
type="checkbox",
|
||||
value="1",
|
||||
:name="inputName",
|
||||
:aria-label="labelSelectItem"
|
||||
)
|
||||
span.pretix-widget-icon-cart(aria-hidden="true")
|
||||
| {{ STRINGS.select }}
|
||||
|
||||
.pretix-widget-item-count-group(v-else, role="group", :aria-label="item.name")
|
||||
button.pretix-widget-btn-default.pretix-widget-item-count-dec(
|
||||
type="button",
|
||||
data-step="-1",
|
||||
:data-controls="`input_${inputName}`",
|
||||
:aria-label="decLabel",
|
||||
@click.prevent.stop="onStep"
|
||||
)
|
||||
span -
|
||||
input.pretix-widget-item-count-multiple(
|
||||
:id="`input_${inputName}`",
|
||||
ref="quantity",
|
||||
type="number",
|
||||
inputmode="numeric",
|
||||
pattern="\\d*",
|
||||
placeholder="0",
|
||||
min="0",
|
||||
:max="orderMax",
|
||||
:name="inputName",
|
||||
:aria-labelledby="ariaLabelledby"
|
||||
)
|
||||
button.pretix-widget-btn-default.pretix-widget-item-count-inc(
|
||||
type="button",
|
||||
data-step="1",
|
||||
:data-controls="`input_${inputName}`",
|
||||
:aria-label="incLabel",
|
||||
@click.prevent.stop="onStep"
|
||||
)
|
||||
span +
|
||||
</template>
|
||||
<style lang="sass">
|
||||
</style>
|
||||
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref } from 'vue'
|
||||
import { StoreKey } from '~/sharedStore'
|
||||
import Overlay from './Overlay.vue'
|
||||
|
||||
const lang = LANG // we need this so the template sees the variable
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
|
||||
const form = ref<HTMLFormElement>()
|
||||
|
||||
const formMethod = computed(() => {
|
||||
if (!store.useIframe && store.isButton && store.items.length === 0) {
|
||||
return 'get'
|
||||
}
|
||||
return 'post'
|
||||
})
|
||||
|
||||
const formAction = computed(() => store.getFormAction())
|
||||
|
||||
const formTarget = computed(() => {
|
||||
const isFirefox = navigator.userAgent.toLowerCase().includes('firefox')
|
||||
const isAndroid = navigator.userAgent.toLowerCase().includes('android')
|
||||
if (isAndroid && isFirefox) {
|
||||
return '_top'
|
||||
}
|
||||
return '_blank'
|
||||
})
|
||||
|
||||
const consentParameterValue = computed(() => {
|
||||
if (store.widgetData.consent) {
|
||||
return encodeURIComponent(store.widgetData.consent)
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const widgetDataJson = computed(() => {
|
||||
const clonedData = { ...store.widgetData }
|
||||
if (clonedData.consent) {
|
||||
delete clonedData.consent
|
||||
}
|
||||
return JSON.stringify(clonedData)
|
||||
})
|
||||
|
||||
function handleBuy (event: Event) {
|
||||
if (form.value) {
|
||||
const formData = new FormData(form.value)
|
||||
store.buy(formData, event)
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
form,
|
||||
buy: handleBuy,
|
||||
})
|
||||
</script>
|
||||
<template lang="pug">
|
||||
.pretix-widget-wrapper
|
||||
.pretix-widget-button-container
|
||||
form(ref="form", :method="formMethod", :action="formAction", :target="formTarget")
|
||||
input(v-if="store.voucherCode", type="hidden", name="_voucher_code", :value="store.voucherCode")
|
||||
input(v-if="store.voucherCode", type="hidden", name="voucher", :value="store.voucherCode")
|
||||
input(type="hidden", name="subevent", :value="store.subevent")
|
||||
input(type="hidden", name="locale", :value="lang")
|
||||
input(type="hidden", name="widget_data", :value="widgetDataJson")
|
||||
input(v-if="consentParameterValue", type="hidden", name="consent", :value="consentParameterValue")
|
||||
input(
|
||||
v-for="item in store.items",
|
||||
:key="item.item",
|
||||
type="hidden",
|
||||
:name="item.item",
|
||||
:value="item.count"
|
||||
)
|
||||
button.pretix-button(@click="handleBuy", v-html="store.buttonText")
|
||||
.pretix-widget-clear
|
||||
|
||||
Overlay
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { Category } from '~/types'
|
||||
import Item from './Item.vue'
|
||||
|
||||
defineProps<{
|
||||
category: Category
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
.pretix-widget-category(:data-id="category.id")
|
||||
h3.pretix-widget-category-name(v-if="category.name") {{ category.name }}
|
||||
.pretix-widget-category-description(v-if="category.description", v-html="category.description")
|
||||
.pretix-widget-category-items
|
||||
Item(
|
||||
v-for="item in category.items",
|
||||
:key="item.id",
|
||||
:item="item",
|
||||
:category="category"
|
||||
)
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
</style>
|
||||
@@ -0,0 +1,120 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref } from 'vue'
|
||||
import { StoreKey } from '~/sharedStore'
|
||||
import { STRINGS } from '~/i18n'
|
||||
import { padNumber } from '~/utils'
|
||||
import EventCalendarRow from './EventCalendarRow.vue'
|
||||
import EventListFilterForm from './EventListFilterForm.vue'
|
||||
|
||||
defineProps<{
|
||||
mobile: boolean
|
||||
}>()
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
const calendar = ref<HTMLDivElement>()
|
||||
|
||||
const displayEventInfo = computed(() => store.displayEventInfo || (store.displayEventInfo === null && store.parentStack.length > 0))
|
||||
|
||||
const monthname = computed(() => {
|
||||
if (!store.date) return ''
|
||||
const monthNum = store.date.substr(5, 2)
|
||||
const year = store.date.substr(0, 4)
|
||||
return `${STRINGS.months[monthNum]} ${year}`
|
||||
})
|
||||
|
||||
const id = computed(() => `${store.htmlId}-event-calendar-table`)
|
||||
|
||||
const ariaLabelledby = computed(() => `${store.htmlId}-event-calendar-table-label`)
|
||||
|
||||
const showFilters = computed(() => !store.disableFilters && store.metaFilterFields.length > 0)
|
||||
|
||||
function backToList () {
|
||||
store.weeks = null
|
||||
store.view = 'events'
|
||||
store.name = null
|
||||
store.frontpageText = null
|
||||
}
|
||||
|
||||
function prevmonth () {
|
||||
if (!store.date) return
|
||||
let curMonth = parseInt(store.date.substr(5, 2))
|
||||
let curYear = parseInt(store.date.substr(0, 4))
|
||||
curMonth--
|
||||
if (curMonth < 1) {
|
||||
curMonth = 12
|
||||
curYear--
|
||||
}
|
||||
store.date = `${curYear}-${padNumber(curMonth, 2)}-01`
|
||||
store.loading++
|
||||
store.reload({ focus: `#${id.value}` })
|
||||
}
|
||||
|
||||
function nextmonth () {
|
||||
if (!store.date) return
|
||||
let curMonth = parseInt(store.date.substr(5, 2))
|
||||
let curYear = parseInt(store.date.substr(0, 4))
|
||||
curMonth++
|
||||
if (curMonth > 12) {
|
||||
curMonth = 1
|
||||
curYear++
|
||||
}
|
||||
store.date = `${curYear}-${padNumber(curMonth, 2)}-01`
|
||||
store.loading++
|
||||
store.reload({ focus: `#${id.value}` })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
.pretix-widget-event-calendar(ref="calendar")
|
||||
//- Back navigation
|
||||
.pretix-widget-back(v-if="store.events !== null")
|
||||
a(href="#", role="button", @click.prevent.stop="backToList")
|
||||
| ‹ {{ STRINGS.back }}
|
||||
|
||||
//- Headline
|
||||
.pretix-widget-event-header(v-if="displayEventInfo")
|
||||
strong {{ store.name }}
|
||||
.pretix-widget-event-description(
|
||||
v-if="displayEventInfo && store.frontpageText",
|
||||
v-html="store.frontpageText"
|
||||
)
|
||||
|
||||
//- Filter
|
||||
EventListFilterForm(v-if="showFilters")
|
||||
|
||||
//- Calendar navigation
|
||||
.pretix-widget-event-calendar-head
|
||||
a.pretix-widget-event-calendar-previous-month(href="#", @click.prevent.stop="prevmonth")
|
||||
| « {{ STRINGS.previous_month }}
|
||||
|
|
||||
strong(:id="ariaLabelledby") {{ monthname }}
|
||||
|
|
||||
a.pretix-widget-event-calendar-next-month(href="#", @click.prevent.stop="nextmonth")
|
||||
| {{ STRINGS.next_month }} »
|
||||
|
||||
//- Calendar table
|
||||
table.pretix-widget-event-calendar-table(
|
||||
:id="id",
|
||||
tabindex="0",
|
||||
:aria-labelledby="ariaLabelledby"
|
||||
)
|
||||
thead
|
||||
tr
|
||||
th(:aria-label="STRINGS.days.MONDAY") {{ STRINGS.days.MO }}
|
||||
th(:aria-label="STRINGS.days.TUESDAY") {{ STRINGS.days.TU }}
|
||||
th(:aria-label="STRINGS.days.WEDNESDAY") {{ STRINGS.days.WE }}
|
||||
th(:aria-label="STRINGS.days.THURSDAY") {{ STRINGS.days.TH }}
|
||||
th(:aria-label="STRINGS.days.FRIDAY") {{ STRINGS.days.FR }}
|
||||
th(:aria-label="STRINGS.days.SATURDAY") {{ STRINGS.days.SA }}
|
||||
th(:aria-label="STRINGS.days.SUNDAY") {{ STRINGS.days.SU }}
|
||||
tbody
|
||||
EventCalendarRow(
|
||||
v-for="(week, idx) in store.weeks",
|
||||
:key="idx",
|
||||
:week="week",
|
||||
:mobile="mobile"
|
||||
)
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
</style>
|
||||
@@ -0,0 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref, onMounted, watch } from 'vue'
|
||||
import type { DayEntry } from '~/types'
|
||||
import { StoreKey } from '~/sharedStore'
|
||||
import EventCalendarEvent from './EventCalendarEvent.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
day: DayEntry | null
|
||||
mobile: boolean
|
||||
}>()
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
const cellEl = ref<HTMLTableCellElement>()
|
||||
|
||||
const daynum = computed(() => {
|
||||
if (!props.day) return ''
|
||||
return props.day.date.substr(8)
|
||||
})
|
||||
|
||||
const dateStr = computed(() => props.day ? new Date(props.day.date).toLocaleDateString() : '')
|
||||
|
||||
const role = computed(() => !props.day || !props.day.events.length || !props.mobile ? 'cell' : 'button')
|
||||
|
||||
const tabindex = computed(() => role.value === 'button' ? '0' : '-1')
|
||||
|
||||
const classObject = computed(() => {
|
||||
const o: Record<string, boolean> = {}
|
||||
if (props.day && props.day.events.length > 0) {
|
||||
o['pretix-widget-has-events'] = true
|
||||
let best = 'red'
|
||||
let allLow = true
|
||||
for (const ev of props.day.events) {
|
||||
if (ev.availability.color === 'green') {
|
||||
best = 'green'
|
||||
if (ev.availability.reason !== 'low') {
|
||||
allLow = false
|
||||
}
|
||||
} else if (ev.availability.color === 'orange' && best !== 'green') {
|
||||
best = 'orange'
|
||||
}
|
||||
}
|
||||
o[`pretix-widget-day-availability-${best}`] = true
|
||||
if (best === 'green' && allLow) {
|
||||
o['pretix-widget-day-availability-low'] = true
|
||||
}
|
||||
}
|
||||
return o
|
||||
})
|
||||
|
||||
function selectDay(e: Event) {
|
||||
if (!props.day || !props.day.events.length || !props.mobile) return
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
if (props.day.events.length === 1) {
|
||||
const ev = props.day.events[0]
|
||||
store.parentStack.push(store.targetUrl)
|
||||
store.targetUrl = ev.event_url
|
||||
store.error = null
|
||||
store.subevent = ev.subevent ?? null
|
||||
store.loading++
|
||||
store.reload()
|
||||
} else {
|
||||
store.events = props.day.events
|
||||
store.view = 'events'
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
const keyDown = e.key ?? e.keyCode
|
||||
if (keyDown === 'Enter' || keyDown === 13 || ['Spacebar', ' '].includes(keyDown as string) || keyDown === 32) {
|
||||
e.preventDefault()
|
||||
selectDay(e)
|
||||
}
|
||||
}
|
||||
|
||||
function attachListeners() {
|
||||
if (role.value === 'button' && cellEl.value) {
|
||||
cellEl.value.addEventListener('click', selectDay)
|
||||
cellEl.value.addEventListener('keydown', onKeyDown)
|
||||
}
|
||||
}
|
||||
|
||||
function detachListeners() {
|
||||
if (cellEl.value) {
|
||||
cellEl.value.removeEventListener('click', selectDay)
|
||||
cellEl.value.removeEventListener('keydown', onKeyDown)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
attachListeners()
|
||||
})
|
||||
|
||||
watch(role, (newValue, oldValue) => {
|
||||
if (newValue === 'button' && oldValue !== 'button') {
|
||||
attachListeners()
|
||||
} else if (newValue !== 'button' && oldValue === 'button') {
|
||||
detachListeners()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
td(
|
||||
ref="cellEl",
|
||||
:class="classObject",
|
||||
:role="role",
|
||||
:tabindex="tabindex",
|
||||
:aria-label="dateStr"
|
||||
)
|
||||
.pretix-widget-event-calendar-day(v-if="day", :aria-label="dateStr") {{ daynum }}
|
||||
.pretix-widget-event-calendar-events(v-if="day")
|
||||
EventCalendarEvent(v-for="e in day.events", :key="e.event_url", :event="e")
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
</style>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
import type { EventEntry } from '~/types'
|
||||
import { StoreKey } from '~/sharedStore'
|
||||
|
||||
const props = defineProps<{
|
||||
event: EventEntry
|
||||
describedby?: string
|
||||
}>()
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
|
||||
const classObject = computed(() => {
|
||||
const o: Record<string, boolean> = {
|
||||
'pretix-widget-event-calendar-event': true,
|
||||
}
|
||||
o[`pretix-widget-event-availability-${props.event.availability.color}`] = true
|
||||
if (props.event.availability.reason) {
|
||||
o[`pretix-widget-event-availability-${props.event.availability.reason}`] = true
|
||||
}
|
||||
return o
|
||||
})
|
||||
|
||||
function select() {
|
||||
store.parentStack.push(store.targetUrl)
|
||||
store.targetUrl = props.event.event_url
|
||||
store.error = null
|
||||
store.subevent = props.event.subevent ?? null
|
||||
store.loading++
|
||||
store.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
a.pretix-widget-event-calendar-event(
|
||||
href="#",
|
||||
:class="classObject",
|
||||
@click.prevent.stop="select",
|
||||
:aria-describedby="describedby"
|
||||
)
|
||||
strong.pretix-widget-event-calendar-event-name {{ event.name }}
|
||||
.pretix-widget-event-calendar-event-date(v-if="!event.continued && event.time") {{ event.time }}
|
||||
.pretix-widget-event-calendar-event-availability(v-if="!event.continued && event.availability.text") {{ event.availability.text }}
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { DayEntry } from '~/types'
|
||||
import EventCalendarCell from './EventCalendarCell.vue'
|
||||
|
||||
defineProps<{
|
||||
week: (DayEntry | null)[]
|
||||
mobile: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
tr
|
||||
EventCalendarCell(v-for="(d, idx) in week", :key="idx", :day="d", :mobile="mobile")
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
</style>
|
||||
@@ -0,0 +1,298 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { StoreKey } from '~/sharedStore'
|
||||
import { STRINGS } from '~/i18n'
|
||||
import Category from './Category.vue'
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
|
||||
const form = ref<HTMLFormElement>()
|
||||
const voucherinput = ref<HTMLInputElement>()
|
||||
const isItemsSelected = ref(false)
|
||||
const localVoucher = ref('')
|
||||
|
||||
const displayEventInfo = computed(() => store.displayEventInfo || (store.displayEventInfo === null && (store.events || store.weeks || store.days)))
|
||||
|
||||
const idVoucherInput = computed(() => `${store.htmlId}-voucher-input`)
|
||||
|
||||
const ariaLabelledby = computed(() => `${store.htmlId}-voucher-headline`)
|
||||
|
||||
const idCartExistsMsg = computed(() => `${store.htmlId}-cart-exists`)
|
||||
|
||||
const buyLabel = computed(() => {
|
||||
let allFree = true
|
||||
for (const cat of store.categories) {
|
||||
for (const item of cat.items) {
|
||||
for (const v of item.variations) {
|
||||
if (v.price.gross !== '0.00') {
|
||||
allFree = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if ((item.variations.length === 0 && item.price.gross !== '0.00') || item.mandatory_priced_addons) {
|
||||
allFree = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!allFree) break
|
||||
}
|
||||
return allFree ? STRINGS.register : STRINGS.buy
|
||||
})
|
||||
|
||||
const hiddenParams = computed(() => {
|
||||
const params = new URL(store.getVoucherFormTarget()).searchParams
|
||||
params.delete('iframe')
|
||||
params.delete('take_cart_id')
|
||||
return Array.from(params.entries())
|
||||
})
|
||||
|
||||
const showVoucherForm = computed(() => store.vouchersExist && !store.disableVouchers && !store.voucherCode)
|
||||
|
||||
const consentParameterValue = computed(() => {
|
||||
if (store.widgetData.consent) {
|
||||
return encodeURIComponent(store.widgetData.consent)
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const widgetDataJson = computed(() => {
|
||||
const clonedData = { ...store.widgetData }
|
||||
if (clonedData.consent) {
|
||||
delete clonedData.consent
|
||||
}
|
||||
return JSON.stringify(clonedData)
|
||||
})
|
||||
|
||||
const formAction = computed(() => {
|
||||
const additionalParams = getAdditionalURLParams()
|
||||
let checkoutUrl = `/${store.targetUrl.replace(/^[^\/]+:\/\/([^\/]+)\//, '')}w/${store.widgetId.replace('pretix-widget-', '')}/`
|
||||
if (!store.cartExists) {
|
||||
checkoutUrl += 'checkout/start'
|
||||
}
|
||||
if (additionalParams) {
|
||||
checkoutUrl += `?${additionalParams}`
|
||||
}
|
||||
|
||||
const cookieName = `pretix_widget_${store.targetUrl.replace(/[^a-zA-Z0-9]+/g, '_')}`
|
||||
const cartIdCookie = document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith(`${cookieName}=`))
|
||||
?.split('=')[1] || null
|
||||
|
||||
let formTarget = `${store.targetUrl}w/${store.widgetId.replace('pretix-widget-', '')}/cart/add?iframe=1&next=${encodeURIComponent(checkoutUrl)}`
|
||||
if (cartIdCookie) {
|
||||
formTarget += `&take_cart_id=${cartIdCookie}`
|
||||
}
|
||||
if (store.widgetData.consent) {
|
||||
formTarget += `&consent=${encodeURIComponent(store.widgetData.consent)}`
|
||||
}
|
||||
return formTarget
|
||||
})
|
||||
|
||||
const formTarget = computed(() => {
|
||||
const isFirefox = navigator.userAgent.toLowerCase().includes('firefox')
|
||||
const isAndroid = navigator.userAgent.toLowerCase().includes('android')
|
||||
if (isAndroid && isFirefox) {
|
||||
return '_top'
|
||||
}
|
||||
return '_blank'
|
||||
})
|
||||
|
||||
function getAdditionalURLParams (): string {
|
||||
if (!window.location.search.includes('utm_')) {
|
||||
return ''
|
||||
}
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
for (const [key] of params.entries()) {
|
||||
if (!key.startsWith('utm_')) {
|
||||
params.delete(key)
|
||||
}
|
||||
}
|
||||
return params.toString()
|
||||
}
|
||||
|
||||
function backToList () {
|
||||
store.targetUrl = store.parentStack.pop() || store.targetUrl
|
||||
store.error = null
|
||||
if (!store.subevent) {
|
||||
store.name = null
|
||||
store.frontpageText = null
|
||||
}
|
||||
store.subevent = null
|
||||
store.offset = 0
|
||||
store.appendEvents = false
|
||||
store.triggerLoadCallback()
|
||||
|
||||
if (store.events !== null) {
|
||||
store.view = 'events'
|
||||
} else if (store.days !== null) {
|
||||
store.view = 'days'
|
||||
} else {
|
||||
store.view = 'weeks'
|
||||
}
|
||||
}
|
||||
|
||||
function calcItemsSelected () {
|
||||
if (!form.value) return
|
||||
const checkboxes = form.value.querySelectorAll<HTMLInputElement>('input[type=checkbox], input[type=radio]')
|
||||
const hasChecked = Array.from(checkboxes).some((el) => el.checked)
|
||||
const numberInputs = form.value.querySelectorAll<HTMLInputElement>('.pretix-widget-item-count-group input')
|
||||
const hasQuantity = Array.from(numberInputs).some((el) => parseInt(el.value || '0') > 0)
|
||||
isItemsSelected.value = hasChecked || hasQuantity
|
||||
}
|
||||
|
||||
function focusVoucherField () {
|
||||
voucherinput.value?.focus()
|
||||
}
|
||||
|
||||
function handleBuy (event: Event) {
|
||||
if (form.value) {
|
||||
const formData = new FormData(form.value)
|
||||
store.buy(formData, event)
|
||||
}
|
||||
}
|
||||
|
||||
function handleRedeem (event: Event) {
|
||||
store.redeem(localVoucher.value, event)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (form.value) {
|
||||
form.value.addEventListener('change', calcItemsSelected)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (form.value) {
|
||||
form.value.removeEventListener('change', calcItemsSelected)
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => store.overlay?.frameShown, (newValue) => {
|
||||
if (!newValue && form.value) {
|
||||
form.value.reset()
|
||||
calcItemsSelected()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
.pretix-widget-event-form
|
||||
//- Back navigation
|
||||
.pretix-widget-event-list-back(v-if="store.events || store.weeks || store.days")
|
||||
a(v-if="!store.subevent", href="#", rel="back", @click.prevent.stop="backToList")
|
||||
| ‹ {{ STRINGS.back_to_list }}
|
||||
a(v-if="store.subevent", href="#", rel="back", @click.prevent.stop="backToList")
|
||||
| ‹ {{ STRINGS.back_to_dates }}
|
||||
|
||||
//- Event name
|
||||
.pretix-widget-event-header(v-if="displayEventInfo")
|
||||
strong(role="heading", aria-level="2") {{ store.name }}
|
||||
|
||||
//- Date range
|
||||
.pretix-widget-event-details(v-if="displayEventInfo && store.dateRange") {{ store.dateRange }}
|
||||
|
||||
//- Location
|
||||
.pretix-widget-event-location(
|
||||
v-if="displayEventInfo && store.location",
|
||||
v-html="store.location"
|
||||
)
|
||||
|
||||
//- Description
|
||||
.pretix-widget-event-description(
|
||||
v-if="displayEventInfo && store.frontpageText",
|
||||
v-html="store.frontpageText"
|
||||
)
|
||||
|
||||
//- Form start
|
||||
form(
|
||||
ref="form",
|
||||
method="post",
|
||||
:action="formAction",
|
||||
:target="formTarget",
|
||||
@submit="handleBuy"
|
||||
)
|
||||
input(v-if="store.voucherCode", type="hidden", name="_voucher_code", :value="store.voucherCode")
|
||||
input(type="hidden", name="subevent", :value="store.subevent")
|
||||
input(type="hidden", name="widget_data", :value="widgetDataJson")
|
||||
input(v-if="consentParameterValue", type="hidden", name="consent", :value="consentParameterValue")
|
||||
|
||||
//- Error message
|
||||
.pretix-widget-error-message(v-if="store.error") {{ store.error }}
|
||||
|
||||
//- Resume cart
|
||||
.pretix-widget-info-message.pretix-widget-clickable(v-if="store.cartExists")
|
||||
span(:id="idCartExistsMsg") {{ STRINGS.cart_exists }}
|
||||
button.pretix-widget-resume-button(
|
||||
type="button",
|
||||
:aria-describedby="idCartExistsMsg",
|
||||
@click.prevent.stop="store.resume()"
|
||||
) {{ STRINGS.resume_checkout }}
|
||||
|
||||
//- Seating plan
|
||||
.pretix-widget-seating-link-wrapper(v-if="store.hasSeatingPlan")
|
||||
button.pretix-widget-seating-link(type="button", @click.prevent.stop="store.startseating()")
|
||||
| {{ STRINGS.show_seating }}
|
||||
|
||||
//- Waiting list for seating plan
|
||||
.pretix-widget-seating-waitinglist(v-if="store.hasSeatingPlan && store.hasSeatingPlanWaitinglist")
|
||||
.pretix-widget-seating-waitinglist-text {{ STRINGS.seating_plan_waiting_list }}
|
||||
.pretix-widget-seating-waitinglist-button-wrap
|
||||
button.pretix-widget-seating-waitinglist-button(@click.prevent.stop="store.startwaiting()")
|
||||
| {{ STRINGS.waiting_list }}
|
||||
.pretix-widget-clear
|
||||
|
||||
//- Product list
|
||||
Category(v-for="category in store.categories", :key="category.id", :category="category")
|
||||
|
||||
//- Buy button
|
||||
.pretix-widget-action(v-if="store.displayAddToCart")
|
||||
button(
|
||||
v-if="!store.cartExists || isItemsSelected",
|
||||
type="submit",
|
||||
:aria-describedby="idCartExistsMsg"
|
||||
) {{ buyLabel }}
|
||||
button(
|
||||
v-else,
|
||||
type="button",
|
||||
:aria-describedby="idCartExistsMsg",
|
||||
@click.prevent.stop="store.resume()"
|
||||
) {{ STRINGS.resume_checkout }}
|
||||
|
||||
//- Voucher form
|
||||
form(
|
||||
v-if="showVoucherForm",
|
||||
method="get",
|
||||
:action="store.getVoucherFormTarget()",
|
||||
target="_blank"
|
||||
)
|
||||
.pretix-widget-voucher
|
||||
h3.pretix-widget-voucher-headline(:id="ariaLabelledby") {{ STRINGS.redeem_voucher }}
|
||||
.pretix-widget-voucher-text(
|
||||
v-if="store.voucherExplanationText",
|
||||
v-html="store.voucherExplanationText"
|
||||
)
|
||||
.pretix-widget-voucher-input-wrap
|
||||
input.pretix-widget-voucher-input(
|
||||
:id="idVoucherInput",
|
||||
ref="voucherinput",
|
||||
v-model="localVoucher",
|
||||
type="text",
|
||||
name="voucher",
|
||||
:placeholder="STRINGS.voucher_code",
|
||||
:aria-labelledby="ariaLabelledby"
|
||||
)
|
||||
input(
|
||||
v-for="p in hiddenParams",
|
||||
:key="p[0]",
|
||||
type="hidden",
|
||||
:name="p[0]",
|
||||
:value="p[1]"
|
||||
)
|
||||
.pretix-widget-voucher-button-wrap
|
||||
button(@click="handleRedeem") {{ STRINGS.redeem }}
|
||||
.pretix-widget-clear
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
</style>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, nextTick } from 'vue'
|
||||
import { StoreKey } from '~/sharedStore'
|
||||
import { STRINGS } from '~/i18n'
|
||||
import EventListEntry from './EventListEntry.vue'
|
||||
import EventListFilterForm from './EventListFilterForm.vue'
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
|
||||
const displayEventInfo = computed(() => store.displayEventInfo || (store.displayEventInfo === null && store.parentStack.length > 0))
|
||||
|
||||
const showBackButton = computed(() => store.weeks || store.parentStack.length > 0)
|
||||
|
||||
const showFilters = computed(() => !store.disableFilters && store.metaFilterFields.length > 0)
|
||||
|
||||
function backToCalendar() {
|
||||
store.offset = 0
|
||||
store.appendEvents = false
|
||||
|
||||
if (store.weeks) {
|
||||
store.events = null
|
||||
store.view = 'weeks'
|
||||
store.name = null
|
||||
store.frontpageText = null
|
||||
} else {
|
||||
store.loading++
|
||||
store.targetUrl = store.parentStack.pop() || store.targetUrl
|
||||
store.error = null
|
||||
store.reload()
|
||||
}
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
store.appendEvents = true
|
||||
store.offset += 50
|
||||
store.loading++
|
||||
store.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
.pretix-widget-event-list
|
||||
.pretix-widget-back(v-if="showBackButton")
|
||||
a(href="#", rel="prev", @click.prevent.stop="backToCalendar")
|
||||
| ‹ {{ STRINGS.back }}
|
||||
|
||||
.pretix-widget-event-header(v-if="displayEventInfo")
|
||||
strong {{ store.name }}
|
||||
|
||||
.pretix-widget-event-description(
|
||||
v-if="displayEventInfo && store.frontpageText",
|
||||
v-html="store.frontpageText"
|
||||
)
|
||||
|
||||
EventListFilterForm(v-if="showFilters")
|
||||
|
||||
EventListEntry(
|
||||
v-for="event in store.events",
|
||||
:key="event.event_url",
|
||||
:event="event"
|
||||
)
|
||||
|
||||
p.pretix-widget-event-list-load-more(v-if="store.hasMoreEvents")
|
||||
button(@click.prevent.stop="loadMore") {{ STRINGS.load_more }}
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
import type { EventEntry } from '~/types'
|
||||
import { StoreKey } from '~/sharedStore'
|
||||
|
||||
const props = defineProps<{
|
||||
event: EventEntry
|
||||
}>()
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
|
||||
const classObject = computed(() => {
|
||||
const o: Record<string, boolean> = {
|
||||
'pretix-widget-event-list-entry': true,
|
||||
}
|
||||
o[`pretix-widget-event-availability-${props.event.availability.color}`] = true
|
||||
if (props.event.availability.reason) {
|
||||
o[`pretix-widget-event-availability-${props.event.availability.reason}`] = true
|
||||
}
|
||||
return o
|
||||
})
|
||||
|
||||
const location = computed(() => props.event.location.replace(/\s*\n\s*/g, ', '))
|
||||
|
||||
function select() {
|
||||
store.parentStack.push(store.targetUrl)
|
||||
store.targetUrl = props.event.event_url
|
||||
store.error = null
|
||||
store.subevent = props.event.subevent ?? null
|
||||
store.loading++
|
||||
store.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
a.pretix-widget-event-list-entry(href="#", :class="classObject", @click.prevent.stop="select")
|
||||
.pretix-widget-event-list-entry-name {{ event.name }}
|
||||
.pretix-widget-event-list-entry-date {{ event.date_range }}
|
||||
.pretix-widget-event-list-entry-location {{ location }}
|
||||
.pretix-widget-event-list-entry-availability
|
||||
span {{ event.availability.text }}
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
import type { MetaFilterField } from '~/types'
|
||||
import { StoreKey, globalWidgetId } from '~/sharedStore'
|
||||
|
||||
const props = defineProps<{
|
||||
field: MetaFilterField
|
||||
}>()
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
|
||||
const id = computed(() => `${globalWidgetId}_${props.field.key}`)
|
||||
|
||||
const currentValue = computed(() => {
|
||||
const filterParams = new URLSearchParams(store.filter || '')
|
||||
return filterParams.get(props.field.key) || ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
.pretix-widget-event-list-filter-field
|
||||
label(:for="id") {{ field.label }}
|
||||
select(:id="id", :name="field.key", :value="currentValue")
|
||||
option(v-for="choice in field.choices", :key="choice[0]", :value="choice[0]") {{ choice[1] }}
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
</style>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { inject, ref } from 'vue'
|
||||
import { StoreKey } from '~/sharedStore'
|
||||
import { STRINGS } from '~/i18n'
|
||||
import EventListFilterField from './EventListFilterField.vue'
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
const filterform = ref<HTMLFormElement>()
|
||||
|
||||
function onSubmit(e: Event) {
|
||||
e.preventDefault()
|
||||
if (!filterform.value) return
|
||||
|
||||
const formData = new FormData(filterform.value)
|
||||
const filterParams = new URLSearchParams()
|
||||
|
||||
formData.forEach((value, key) => {
|
||||
if (value !== '') {
|
||||
filterParams.set(key, value as string)
|
||||
}
|
||||
})
|
||||
|
||||
store.filter = filterParams.toString()
|
||||
store.loading++
|
||||
store.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
form.pretix-widget-event-list-filter-form(ref="filterform", @submit="onSubmit")
|
||||
fieldset.pretix-widget-event-list-filter-fieldset
|
||||
legend {{ STRINGS.filter_events_by }}
|
||||
EventListFilterField(
|
||||
v-for="field in store.metaFilterFields",
|
||||
:key="field.key",
|
||||
:field="field"
|
||||
)
|
||||
button {{ STRINGS.filter }}
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
</style>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref } from 'vue'
|
||||
import { StoreKey } from '~/sharedStore'
|
||||
import { STRINGS } from '~/i18n'
|
||||
import { getISOWeeks } from '~/utils'
|
||||
import EventWeekCell from './EventWeekCell.vue'
|
||||
import EventListFilterForm from './EventListFilterForm.vue'
|
||||
|
||||
defineProps<{
|
||||
mobile: boolean
|
||||
}>()
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
const weekcalendar = ref<HTMLDivElement>()
|
||||
|
||||
const displayEventInfo = computed(() => store.displayEventInfo || (store.displayEventInfo === null && store.parentStack.length > 0))
|
||||
|
||||
const weekname = computed(() => {
|
||||
if (!store.week) return ''
|
||||
const curWeek = store.week[1]
|
||||
const curYear = store.week[0]
|
||||
return `${curWeek} / ${curYear}`
|
||||
})
|
||||
|
||||
const id = computed(() => `${store.htmlId}-event-week-table`)
|
||||
|
||||
const showFilters = computed(() => !store.disableFilters && store.metaFilterFields.length > 0)
|
||||
|
||||
function backToList() {
|
||||
store.weeks = null
|
||||
store.name = null
|
||||
store.frontpageText = null
|
||||
store.view = 'events'
|
||||
}
|
||||
|
||||
function prevweek() {
|
||||
if (!store.week) return
|
||||
let curWeek = store.week[1]
|
||||
let curYear = store.week[0]
|
||||
curWeek--
|
||||
if (curWeek < 1) {
|
||||
curYear--
|
||||
curWeek = getISOWeeks(curYear)
|
||||
}
|
||||
store.week = [curYear, curWeek]
|
||||
store.loading++
|
||||
store.reload({ focus: `#${id.value}` })
|
||||
}
|
||||
|
||||
function nextweek() {
|
||||
if (!store.week) return
|
||||
let curWeek = store.week[1]
|
||||
let curYear = store.week[0]
|
||||
curWeek++
|
||||
if (curWeek > getISOWeeks(curYear)) {
|
||||
curWeek = 1
|
||||
curYear++
|
||||
}
|
||||
store.week = [curYear, curWeek]
|
||||
store.loading++
|
||||
store.reload({ focus: `#${id.value}` })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
.pretix-widget-event-calendar.pretix-widget-event-week-calendar(ref="weekcalendar")
|
||||
//- Back navigation
|
||||
.pretix-widget-back(v-if="store.events !== null")
|
||||
a(href="#", @click.prevent.stop="backToList", role="button")
|
||||
| ‹ {{ STRINGS.back }}
|
||||
|
||||
//- Event header
|
||||
.pretix-widget-event-header(v-if="displayEventInfo")
|
||||
strong {{ store.name }}
|
||||
|
||||
//- Filter
|
||||
EventListFilterForm(v-if="showFilters")
|
||||
|
||||
//- Calendar navigation
|
||||
.pretix-widget-event-description(
|
||||
v-if="store.frontpageText && displayEventInfo",
|
||||
v-html="store.frontpageText"
|
||||
)
|
||||
.pretix-widget-event-calendar-head
|
||||
a.pretix-widget-event-calendar-previous-month(href="#", @click.prevent.stop="prevweek", role="button")
|
||||
| « {{ STRINGS.previous_week }}
|
||||
|
|
||||
strong {{ weekname }}
|
||||
|
|
||||
a.pretix-widget-event-calendar-next-month(href="#", @click.prevent.stop="nextweek", role="button")
|
||||
| {{ STRINGS.next_week }} »
|
||||
|
||||
//- Actual calendar
|
||||
.pretix-widget-event-week-table(:id="id", tabindex="0", :aria-label="weekname")
|
||||
.pretix-widget-event-week-col(v-for="d in store.days", :key="d?.date || ''")
|
||||
EventWeekCell(:day="d", :mobile="mobile")
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
import type { DayEntry } from '~/types'
|
||||
import { StoreKey } from '~/sharedStore'
|
||||
import EventCalendarEvent from './EventCalendarEvent.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
day: DayEntry | null
|
||||
mobile: boolean
|
||||
}>()
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
|
||||
const id = computed(() => props.day ? `${store.htmlId}-${props.day.date}` : '')
|
||||
|
||||
const dayhead = computed(() => {
|
||||
if (!props.day) return ''
|
||||
return props.day.day_formatted
|
||||
})
|
||||
|
||||
const classObject = computed(() => {
|
||||
const o: Record<string, boolean> = {}
|
||||
if (props.day && props.day.events.length > 0) {
|
||||
o['pretix-widget-has-events'] = true
|
||||
let best = 'red'
|
||||
let allLow = true
|
||||
for (const ev of props.day.events) {
|
||||
if (ev.availability.color === 'green') {
|
||||
best = 'green'
|
||||
if (ev.availability.reason !== 'low') {
|
||||
allLow = false
|
||||
}
|
||||
} else if (ev.availability.color === 'orange' && best !== 'green') {
|
||||
best = 'orange'
|
||||
}
|
||||
}
|
||||
o[`pretix-widget-day-availability-${best}`] = true
|
||||
if (best === 'green' && allLow) {
|
||||
o['pretix-widget-day-availability-low'] = true
|
||||
}
|
||||
}
|
||||
return o
|
||||
})
|
||||
|
||||
function selectDay () {
|
||||
if (!props.day || !props.day.events.length || !props.mobile) return
|
||||
|
||||
if (props.day.events.length === 1) {
|
||||
const ev = props.day.events[0]
|
||||
store.parentStack.push(store.targetUrl)
|
||||
store.targetUrl = ev.event_url
|
||||
store.error = null
|
||||
store.subevent = ev.subevent ?? null
|
||||
store.loading++
|
||||
store.reload()
|
||||
} else {
|
||||
store.events = props.day.events
|
||||
store.view = 'events'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
div(:class="classObject", @click.prevent.stop="selectDay")
|
||||
.pretix-widget-event-calendar-day(v-if="day", :id="id") {{ dayhead }}
|
||||
.pretix-widget-event-calendar-events(v-if="day")
|
||||
EventCalendarEvent(
|
||||
v-for="e in day.events",
|
||||
:key="e.event_url",
|
||||
:event="e",
|
||||
:describedby="id"
|
||||
)
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
</style>
|
||||
218
src/pretix/static/pretixpresale/widget/src/components/Item.vue
Normal file
218
src/pretix/static/pretixpresale/widget/src/components/Item.vue
Normal file
@@ -0,0 +1,218 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref, watch, onMounted, nextTick } from 'vue'
|
||||
import type { Item, Category } from '~/types'
|
||||
import { StoreKey } from '~/sharedStore'
|
||||
import { STRINGS, interpolate } from '~/i18n'
|
||||
import { floatformat } from '~/utils'
|
||||
import AvailBox from './AvailBox.vue'
|
||||
import PriceBox from './PriceBox.vue'
|
||||
import Variation from './Variation.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
item: Item
|
||||
category: Category
|
||||
}>()
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
|
||||
const expanded = ref(store.showVariationsExpanded)
|
||||
const variations = ref<HTMLDivElement>()
|
||||
|
||||
const classObject = computed(() => ({
|
||||
'pretix-widget-item': true,
|
||||
'pretix-widget-item-with-picture': !!props.item.picture,
|
||||
'pretix-widget-item-with-variations': props.item.has_variations,
|
||||
}))
|
||||
|
||||
const varClasses = computed(() => ({
|
||||
'pretix-widget-item-variations': true,
|
||||
'pretix-widget-item-variations-expanded': expanded.value,
|
||||
}))
|
||||
|
||||
const pictureAltText = computed(() => interpolate(STRINGS.image_of, [props.item.name]))
|
||||
|
||||
const headingLevel = computed(() => props.category.name ? '4' : '3')
|
||||
|
||||
const itemLabelId = computed(() => `${store.htmlId}-item-label-${props.item.id}`)
|
||||
|
||||
const itemDescId = computed(() => `${store.htmlId}-item-desc-${props.item.id}`)
|
||||
|
||||
const itemPriceId = computed(() => `${store.htmlId}-item-price-${props.item.id}`)
|
||||
|
||||
const ariaLabelledby = computed(() => `${itemLabelId.value} ${itemPriceId.value}`)
|
||||
|
||||
const minOrderStr = computed(() => interpolate(STRINGS.order_min, [props.item.order_min]))
|
||||
|
||||
const quotaLeftStr = computed(() => interpolate(STRINGS.quota_left, [props.item.avail[1]]))
|
||||
|
||||
const showToggle = computed(() => props.item.has_variations && !store.showVariationsExpanded)
|
||||
|
||||
// TODO dedupe?
|
||||
const showPrices = computed(() => {
|
||||
let hasPriced = false
|
||||
let cntItems = 0
|
||||
for (const cat of store.categories) {
|
||||
for (const item of cat.items) {
|
||||
if (item.has_variations) {
|
||||
cntItems += item.variations.length
|
||||
hasPriced = true
|
||||
} else {
|
||||
cntItems++
|
||||
hasPriced = hasPriced || item.price.gross !== '0.00' || item.free_price
|
||||
}
|
||||
}
|
||||
}
|
||||
return hasPriced || cntItems > 1
|
||||
})
|
||||
|
||||
// TODO XSS
|
||||
const pricerange = computed(() => {
|
||||
if (props.item.free_price) {
|
||||
return interpolate(
|
||||
STRINGS.price_from,
|
||||
{
|
||||
currency: store.currency,
|
||||
price: floatformat(props.item.min_price || '0', 2),
|
||||
},
|
||||
true
|
||||
).replace(
|
||||
store.currency,
|
||||
`<span class="pretix-widget-pricebox-currency">${store.currency}</span>`
|
||||
)
|
||||
} else if (props.item.min_price !== props.item.max_price) {
|
||||
return `<span class="pretix-widget-pricebox-currency">${store.currency}</span> ${floatformat(props.item.min_price || '0', 2)} – ${floatformat(props.item.max_price || '0', 2)}`
|
||||
} else if (props.item.min_price === '0.00' && props.item.max_price === '0.00') {
|
||||
if (props.item.mandatory_priced_addons) {
|
||||
return '\xA0' // nbsp, because an empty string would cause the HTML element to collapse
|
||||
}
|
||||
return STRINGS.free
|
||||
} else {
|
||||
return `<span class="pretix-widget-pricebox-currency">${store.currency}</span> ${floatformat(props.item.min_price || '0', 2)}`
|
||||
}
|
||||
})
|
||||
|
||||
const variationsToggleLabel = computed(() => expanded.value ? STRINGS.hide_variations : STRINGS.variations)
|
||||
|
||||
function expand () {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
|
||||
function lightbox () {
|
||||
if (store.overlay) {
|
||||
store.overlay.lightbox = {
|
||||
image: props.item.picture_fullsize || '',
|
||||
description: props.item.name,
|
||||
loading: true, // TODO why?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (variations.value && !expanded.value) {
|
||||
variations.value.hidden = true
|
||||
|
||||
variations.value.addEventListener('transitionend', function (event) {
|
||||
if (event.target === variations.value) {
|
||||
if (variations.value) {
|
||||
variations.value.hidden = !expanded.value
|
||||
variations.value.style.maxHeight = 'none'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
watch(expanded, (newValue) => {
|
||||
const v = variations.value
|
||||
if (!v) return
|
||||
|
||||
v.hidden = false
|
||||
v.style.maxHeight = `${newValue ? 0 : v.scrollHeight}px`
|
||||
|
||||
// Vue.nextTick does not work here
|
||||
setTimeout(() => {
|
||||
v.style.maxHeight = `${!newValue ? 0 : v.scrollHeight}px`
|
||||
}, 50)
|
||||
})
|
||||
</script>
|
||||
<template lang="pug">
|
||||
div(
|
||||
:class="classObject",
|
||||
:data-id="item.id",
|
||||
role="group",
|
||||
:aria-labelledby="ariaLabelledby",
|
||||
:aria-describedby="itemDescId"
|
||||
)
|
||||
.pretix-widget-item-row.pretix-widget-main-item-row
|
||||
//- Product description
|
||||
.pretix-widget-item-info-col
|
||||
a.pretix-widget-item-picture-link(
|
||||
v-if="item.picture",
|
||||
:href="item.picture_fullsize",
|
||||
@click.prevent.stop="lightbox"
|
||||
)
|
||||
img.pretix-widget-item-picture(:src="item.picture", :alt="pictureAltText")
|
||||
.pretix-widget-item-title-and-description
|
||||
strong.pretix-widget-item-title(
|
||||
:id="itemLabelId",
|
||||
role="heading",
|
||||
:aria-level="headingLevel"
|
||||
) {{ item.name }}
|
||||
.pretix-widget-item-description(
|
||||
v-if="item.description",
|
||||
:id="itemDescId",
|
||||
v-html="item.description"
|
||||
)
|
||||
p.pretix-widget-item-meta(v-if="item.order_min && item.order_min > 1")
|
||||
small {{ minOrderStr }}
|
||||
p.pretix-widget-item-meta(
|
||||
v-if="!item.has_variations && item.avail[1] !== null && item.avail[0] === 100"
|
||||
)
|
||||
small {{ quotaLeftStr }}
|
||||
|
||||
//- Price
|
||||
.pretix-widget-item-price-col(:id="itemPriceId")
|
||||
PriceBox(
|
||||
v-if="!item.has_variations && showPrices",
|
||||
:price="item.price",
|
||||
:freePrice="item.free_price",
|
||||
:mandatoryPricedAddons="item.mandatory_priced_addons",
|
||||
:suggestedPrice="item.suggested_price",
|
||||
:fieldName="`price_${item.id}`",
|
||||
:originalPrice="item.original_price",
|
||||
:itemId="item.id"
|
||||
)
|
||||
.pretix-widget-pricebox(v-if="item.has_variations && showPrices", v-html="pricerange")
|
||||
span(v-if="!showPrices")
|
||||
|
||||
//- Availability
|
||||
.pretix-widget-item-availability-col
|
||||
button.pretix-widget-collapse-indicator(
|
||||
v-if="showToggle",
|
||||
type="button",
|
||||
:aria-expanded="expanded ? 'true' : 'false'",
|
||||
:aria-controls="`${item.id}-variants`",
|
||||
:aria-describedby="itemDescId",
|
||||
@click.prevent.stop="expand"
|
||||
) {{ variationsToggleLabel }}
|
||||
AvailBox(v-if="!item.has_variations", :item="item")
|
||||
|
||||
.pretix-widget-clear
|
||||
|
||||
//- Variations
|
||||
div(
|
||||
v-if="item.has_variations",
|
||||
:id="`${item.id}-variants`",
|
||||
ref="variations",
|
||||
:class="varClasses"
|
||||
)
|
||||
Variation(
|
||||
v-for="variation in item.variations",
|
||||
:key="variation.id",
|
||||
:variation="variation",
|
||||
:item="item",
|
||||
:category="category"
|
||||
)
|
||||
</template>
|
||||
<style lang="sass">
|
||||
</style>
|
||||
@@ -0,0 +1,232 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, onMounted, onUnmounted, inject, nextTick } from 'vue'
|
||||
import { StoreKey } from '~/sharedStore'
|
||||
import { STRINGS } from '~/i18n'
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
|
||||
const cancelBlocked = ref(false)
|
||||
const lightboxImage = ref<HTMLImageElement>()
|
||||
const frameDialog = ref<HTMLDialogElement>()
|
||||
const alertDialog = ref<HTMLDialogElement>()
|
||||
const lightboxDialog = ref<HTMLDialogElement>()
|
||||
const iframe = ref<HTMLIFrameElement>()
|
||||
const closeButton = ref<HTMLButtonElement>()
|
||||
|
||||
const frameClasses = computed(() => ({
|
||||
'pretix-widget-frame-holder': true,
|
||||
'pretix-widget-frame-shown': store.overlay.frameShown || store.overlay.frameLoading,
|
||||
'pretix-widget-frame-isloading': store.overlay.frameLoading,
|
||||
}))
|
||||
|
||||
const alertClasses = computed(() => ({
|
||||
'pretix-widget-alert-holder': true,
|
||||
'pretix-widget-alert-shown': store.overlay.errorMessage,
|
||||
}))
|
||||
|
||||
const lightboxClasses = computed(() => ({
|
||||
'pretix-widget-lightbox-holder': true,
|
||||
'pretix-widget-lightbox-shown': store.overlay.lightbox,
|
||||
'pretix-widget-lightbox-isloading': store.overlay.lightbox?.loading,
|
||||
}))
|
||||
|
||||
const cancelBlockedClasses = computed(() => ({
|
||||
'pretix-widget-visibility-hidden': !cancelBlocked.value,
|
||||
}))
|
||||
|
||||
const errorMessageId = computed(() => `${store.htmlId}-error-message`)
|
||||
|
||||
function onMessage (e: MessageEvent) {
|
||||
if (e.data.type && e.data.type === 'pretix:widget:title') {
|
||||
if (iframe.value) {
|
||||
iframe.value.title = e.data.title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function lightboxClose () {
|
||||
store.overlay.lightbox = null
|
||||
}
|
||||
|
||||
function lightboxLoaded () {
|
||||
if (store.overlay.lightbox) {
|
||||
store.overlay.lightbox.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
function errorClose (e: Event) {
|
||||
const dialog = e.target as HTMLDialogElement
|
||||
if (dialog.returnValue === 'continue' && store.overlay.errorUrlAfter) {
|
||||
if (store.overlay.errorUrlAfterNewTab) {
|
||||
window.open(store.overlay.errorUrlAfter)
|
||||
} else {
|
||||
store.overlay.frameSrc = store.overlay.errorUrlAfter
|
||||
store.overlay.frameLoading = true
|
||||
}
|
||||
}
|
||||
store.overlay.errorMessage = null
|
||||
store.overlay.errorUrlAfter = null
|
||||
store.overlay.errorUrlAfterNewTab = false
|
||||
}
|
||||
|
||||
function close () {
|
||||
if (store.overlay.frameLoading) {
|
||||
frameDialog.value?.showModal()
|
||||
return
|
||||
}
|
||||
store.overlay.frameShown = false
|
||||
store.frameDismissed = true
|
||||
store.overlay.frameSrc = ''
|
||||
store.reload()
|
||||
triggerCloseCallback()
|
||||
}
|
||||
|
||||
function cancel (e: Event) {
|
||||
if (store.overlay.frameLoading) {
|
||||
e.preventDefault()
|
||||
const target = e.target as HTMLElement
|
||||
target.addEventListener('animationend', function () {
|
||||
target.classList.remove('pretix-widget-shake-once')
|
||||
}, { once: true })
|
||||
target.classList.add('pretix-widget-shake-once')
|
||||
cancelBlocked.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function iframeLoaded () {
|
||||
if (store.overlay.frameLoading) {
|
||||
store.overlay.frameLoading = false
|
||||
cancelBlocked.value = false
|
||||
if (store.overlay.frameSrc) {
|
||||
store.overlay.frameShown = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function triggerCloseCallback () {
|
||||
nextTick(() => {
|
||||
for (const callback of (window as any).PretixWidget._closed || []) {
|
||||
callback()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => store.overlay.lightbox, (newValue, oldValue) => {
|
||||
if (newValue) {
|
||||
if (newValue.image !== oldValue?.image) {
|
||||
newValue.loading = true
|
||||
}
|
||||
if (!oldValue) {
|
||||
lightboxDialog.value?.showModal()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => store.overlay.errorMessage, (newValue, oldValue) => {
|
||||
if (newValue && !oldValue) {
|
||||
alertDialog.value?.showModal()
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => store.overlay.frameShown, (newValue) => {
|
||||
if (newValue) {
|
||||
nextTick(() => {
|
||||
closeButton.value?.focus()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => store.overlay.frameSrc, (newValue, oldValue) => {
|
||||
if (newValue && !oldValue) {
|
||||
store.overlay.frameLoading = true
|
||||
}
|
||||
if (iframe.value) {
|
||||
iframe.value.src = newValue || 'about:blank'
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => store.overlay.frameLoading, (newValue) => {
|
||||
if (newValue) {
|
||||
if (frameDialog.value && !frameDialog.value.open) {
|
||||
frameDialog.value.showModal()
|
||||
}
|
||||
} else {
|
||||
if (!store.overlay.frameSrc && frameDialog.value?.open) {
|
||||
frameDialog.value.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('message', onMessage, false)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('message', onMessage, false)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
Teleport(to="body")
|
||||
.pretix-widget-overlay
|
||||
//- Iframe dialog
|
||||
dialog(ref="frameDialog", :class="frameClasses", :aria-label="STRINGS.checkout", @close="close", @cancel="cancel")
|
||||
.pretix-widget-frame-loading(v-show="store.overlay.frameLoading")
|
||||
svg(width="256", height="256", viewBox="0 0 1792 1792", xmlns="http://www.w3.org/2000/svg")
|
||||
path.pretix-widget-primary-color(d="M1152 896q0-106-75-181t-181-75-181 75-75 181 75 181 181 75 181-75 75-181zm512-109v222q0 12-8 23t-20 13l-185 28q-19 54-39 91 35 50 107 138 10 12 10 25t-9 23q-27 37-99 108t-94 71q-12 0-26-9l-138-108q-44 23-91 38-16 136-29 186-7 28-36 28h-222q-14 0-24.5-8.5t-11.5-21.5l-28-184q-49-16-90-37l-141 107q-10 9-25 9-14 0-25-11-126-114-165-168-7-10-7-23 0-12 8-23 15-21 51-66.5t54-70.5q-27-50-41-99l-183-27q-13-2-21-12.5t-8-23.5v-222q0-12 8-23t19-13l186-28q14-46 39-92-40-57-107-138-10-12-10-24 0-10 9-23 26-36 98.5-107.5t94.5-71.5q13 0 26 10l138 107q44-23 91-38 16-136 29-186 7-28 36-28h222q14 0 24.5 8.5t11.5 21.5l28 184q49 16 90 37l142-107q9-9 24-9 13 0 25 10 129 119 165 170 7 8 7 22 0 12-8 23-15 21-51 66.5t-54 70.5q26 50 41 98l183 28q13 2 21 12.5t8 23.5z")
|
||||
p(:class="cancelBlockedClasses")
|
||||
strong {{ STRINGS.cancel_blocked }}
|
||||
.pretix-widget-frame-inner(v-show="store.overlay.frameShown")
|
||||
form.pretix-widget-frame-close(method="dialog")
|
||||
button(ref="closeButton", :aria-label="STRINGS.close_checkout", autofocus)
|
||||
svg(:alt="STRINGS.close", height="16", viewBox="0 0 512 512", width="16", xmlns="http://www.w3.org/2000/svg")
|
||||
path(fill="#fff", d="M437.5,386.6L306.9,256l130.6-130.6c14.1-14.1,14.1-36.8,0-50.9c-14.1-14.1-36.8-14.1-50.9,0L256,205.1L125.4,74.5 c-14.1-14.1-36.8-14.1-50.9,0c-14.1,14.1-14.1,36.8,0,50.9L205.1,256L74.5,386.6c-14.1,14.1-14.1,36.8,0,50.9 c14.1,14.1,36.8,14.1,50.9,0L256,306.9l130.6,130.6c14.1,14.1,36.8,14.1,50.9,0C451.5,423.4,451.5,400.6,437.5,386.6z")
|
||||
iframe(
|
||||
ref="iframe",
|
||||
frameborder="0",
|
||||
width="650",
|
||||
height="650",
|
||||
:name="store.widgetId",
|
||||
src="about:blank",
|
||||
allow="autoplay *; camera *; fullscreen *; payment *",
|
||||
:title="STRINGS.checkout",
|
||||
referrerpolicy="origin",
|
||||
@load="iframeLoaded"
|
||||
) Please enable frames in your browser!
|
||||
|
||||
//- Alert dialog
|
||||
dialog(ref="alertDialog", :class="alertClasses", role="alertdialog", :aria-labelledby="errorMessageId", @close="errorClose")
|
||||
form.pretix-widget-alert-box(method="dialog")
|
||||
p(:id="errorMessageId") {{ store.overlay.errorMessage }}
|
||||
p
|
||||
button(v-if="store.overlay.errorUrlAfter", value="continue", autofocus, :aria-describedby="errorMessageId")
|
||||
| {{ STRINGS.continue }}
|
||||
button(v-else, autofocus, :aria-describedby="errorMessageId") {{ STRINGS.close }}
|
||||
transition(name="bounce")
|
||||
svg.pretix-widget-alert-icon(v-if="store.overlay.errorMessage", width="64", height="64", viewBox="0 0 1792 1792", xmlns="http://www.w3.org/2000/svg")
|
||||
path(style="fill:#ffffff;", d="M 599.86438,303.72882 H 1203.5254 V 1503.4576 H 599.86438 Z")
|
||||
path.pretix-widget-primary-color(d="M896 128q209 0 385.5 103t279.5 279.5 103 385.5-103 385.5-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103zm128 1247v-190q0-14-9-23.5t-22-9.5h-192q-13 0-23 10t-10 23v190q0 13 10 23t23 10h192q13 0 22-9.5t9-23.5zm-2-344l18-621q0-12-10-18-10-8-24-8h-220q-14 0-24 8-10 6-10 18l17 621q0 10 10 17.5t24 7.5h185q14 0 23.5-7.5t10.5-17.5z")
|
||||
|
||||
//- Lightbox dialog
|
||||
dialog(ref="lightboxDialog", :class="lightboxClasses", role="alertdialog", @close="lightboxClose")
|
||||
.pretix-widget-lightbox-loading(v-if="store.overlay.lightbox?.loading")
|
||||
svg(width="256", height="256", viewBox="0 0 1792 1792", xmlns="http://www.w3.org/2000/svg")
|
||||
path.pretix-widget-primary-color(d="M1152 896q0-106-75-181t-181-75-181 75-75 181 75 181 181 75 181-75 75-181zm512-109v222q0 12-8 23t-20 13l-185 28q-19 54-39 91 35 50 107 138 10 12 10 25t-9 23q-27 37-99 108t-94 71q-12 0-26-9l-138-108q-44 23-91 38-16 136-29 186-7 28-36 28h-222q-14 0-24.5-8.5t-11.5-21.5l-28-184q-49-16-90-37l-141 107q-10 9-25 9-14 0-25-11-126-114-165-168-7-10-7-23 0-12 8-23 15-21 51-66.5t54-70.5q-27-50-41-99l-183-27q-13-2-21-12.5t-8-23.5v-222q0-12 8-23t19-13l186-28q14-46 39-92-40-57-107-138-10-12-10-24 0-10 9-23 26-36 98.5-107.5t94.5-71.5q13 0 26 10l138 107q44-23 91-38 16-136 29-186 7-28 36-28h222q14 0 24.5 8.5t11.5 21.5l28 184q49 16 90 37l142-107q9-9 24-9 13 0 25 10 129 119 165 170 7 8 7 22 0 12-8 23-15 21-51 66.5t-54 70.5q26 50 41 98l183 28q13 2 21 12.5t8 23.5z")
|
||||
.pretix-widget-lightbox-inner(v-if="store.overlay.lightbox")
|
||||
form.pretix-widget-lightbox-close(method="dialog")
|
||||
button(:aria-label="STRINGS.close", autofocus)
|
||||
svg(:alt="STRINGS.close", height="16", viewBox="0 0 512 512", width="16", xmlns="http://www.w3.org/2000/svg")
|
||||
path(fill="#fff", d="M437.5,386.6L306.9,256l130.6-130.6c14.1-14.1,14.1-36.8,0-50.9c-14.1-14.1-36.8-14.1-50.9,0L256,205.1L125.4,74.5 c-14.1-14.1-36.8-14.1-50.9,0c-14.1,14.1-14.1,36.8,0,50.9L205.1,256L74.5,386.6c-14.1,14.1-14.1,36.8,0,50.9 c14.1,14.1,36.8,14.1,50.9,0L256,306.9l130.6,130.6c14.1,14.1,36.8,14.1,50.9,0C451.5,423.4,451.5,400.6,437.5,386.6z")
|
||||
figure.pretix-widget-lightbox-image
|
||||
img(
|
||||
ref="lightboxImage",
|
||||
:src="store.overlay.lightbox.image",
|
||||
:alt="store.overlay.lightbox.description",
|
||||
crossorigin,
|
||||
@load="lightboxLoaded"
|
||||
)
|
||||
figcaption(v-if="store.overlay.lightbox.description") {{ store.overlay.lightbox.description }}
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
</style>
|
||||
@@ -0,0 +1,114 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
import type { Price } from '~/types'
|
||||
import { StoreKey } from '~/sharedStore'
|
||||
import { STRINGS, interpolate } from '~/i18n'
|
||||
import { floatformat, autofloatformat, stripHTML } from '~/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
price: Price
|
||||
freePrice: boolean
|
||||
fieldName: string
|
||||
suggestedPrice?: Price | null
|
||||
originalPrice?: string | null
|
||||
mandatoryPricedAddons?: boolean
|
||||
itemId: number
|
||||
}>()
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
|
||||
const priceBoxId = computed(() => `${store.htmlId}-item-pricebox-${props.itemId}`)
|
||||
|
||||
const priceDescId = computed(() => `${store.htmlId}-item-pricedesc-${props.itemId}`)
|
||||
|
||||
const ariaLabelledby = computed(() => `${store.htmlId}-item-label-${props.itemId} ${priceBoxId.value}`)
|
||||
|
||||
const displayPrice = computed(() => {
|
||||
if (store.displayNetPrices) {
|
||||
return floatformat(parseFloat(props.price.net), 2)
|
||||
}
|
||||
return floatformat(parseFloat(props.price.gross), 2)
|
||||
})
|
||||
|
||||
const displayPriceNonlocalized = computed(() => {
|
||||
if (store.displayNetPrices) {
|
||||
return parseFloat(props.price.net).toFixed(2)
|
||||
}
|
||||
return parseFloat(props.price.gross).toFixed(2)
|
||||
})
|
||||
|
||||
const suggestedPriceNonlocalized = computed(() => {
|
||||
const price = props.suggestedPrice ?? props.price
|
||||
if (store.displayNetPrices) {
|
||||
return parseFloat(price.net).toFixed(2)
|
||||
}
|
||||
return parseFloat(price.gross).toFixed(2)
|
||||
})
|
||||
|
||||
// TODO BAD
|
||||
const originalLine = computed(() => {
|
||||
if (!props.originalPrice) return ''
|
||||
return `<span class="pretix-widget-pricebox-currency">${store.currency}</span> ${floatformat(parseFloat(props.originalPrice), 2)}`
|
||||
})
|
||||
|
||||
// TODO BAD
|
||||
const priceline = computed(() => {
|
||||
if (props.price.gross === '0.00') {
|
||||
if (props.mandatoryPricedAddons && !props.originalPrice) {
|
||||
return '\u00A0' // nbsp
|
||||
}
|
||||
return STRINGS.free
|
||||
}
|
||||
return `<span class="pretix-widget-pricebox-currency">${store.currency}</span> ${displayPrice.value}`
|
||||
})
|
||||
|
||||
const originalPriceAriaLabel = computed(() => interpolate(STRINGS.original_price, [stripHTML(originalLine.value)]))
|
||||
|
||||
const newPriceAriaLabel = computed(() => interpolate(STRINGS.new_price, [stripHTML(priceline.value)]))
|
||||
|
||||
const taxline = computed(() => {
|
||||
if (store.displayNetPrices) {
|
||||
if (props.price.includes_mixed_tax_rate) {
|
||||
return STRINGS.tax_plus_mixed
|
||||
}
|
||||
return interpolate(STRINGS.tax_plus, {
|
||||
rate: autofloatformat(props.price.rate, 2),
|
||||
taxname: props.price.name,
|
||||
}, true)
|
||||
} else {
|
||||
if (props.price.includes_mixed_tax_rate) {
|
||||
return STRINGS.tax_incl_mixed
|
||||
}
|
||||
return interpolate(STRINGS.tax_incl, {
|
||||
rate: autofloatformat(props.price.rate, 2),
|
||||
taxname: props.price.name,
|
||||
}, true)
|
||||
}
|
||||
})
|
||||
|
||||
const showTaxline = computed(() => props.price.rate !== '0.00' && props.price.gross !== '0.00')
|
||||
</script>
|
||||
<template lang="pug">
|
||||
.pretix-widget-pricebox
|
||||
span(v-if="!freePrice && !originalPrice", v-html="priceline")
|
||||
span(v-if="!freePrice && originalPrice")
|
||||
del.pretix-widget-pricebox-original-price(:aria-label="originalPriceAriaLabel", v-html="originalLine")
|
||||
|
|
||||
ins.pretix-widget-pricebox-new-price(:aria-label="newPriceAriaLabel", v-html="priceline")
|
||||
div(v-if="freePrice")
|
||||
span.pretix-widget-pricebox-currency(:id="priceBoxId") {{ store.currency }}
|
||||
|
|
||||
input.pretix-widget-pricebox-price-input(
|
||||
type="number",
|
||||
placeholder="0",
|
||||
:min="displayPriceNonlocalized",
|
||||
:value="suggestedPriceNonlocalized",
|
||||
:name="fieldName",
|
||||
step="any",
|
||||
:aria-labelledby="ariaLabelledby",
|
||||
:aria-describedby="priceDescId"
|
||||
)
|
||||
small.pretix-widget-pricebox-tax(v-if="showTaxline", :id="priceDescId") {{ taxline }}
|
||||
</template>
|
||||
<style lang="sass">
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
import type { Variation, Item, Category } from '~/types'
|
||||
import { StoreKey } from '~/sharedStore'
|
||||
import { STRINGS, interpolate } from '~/i18n'
|
||||
import AvailBox from './AvailBox.vue'
|
||||
import PriceBox from './PriceBox.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
variation: Variation
|
||||
item: Item
|
||||
category: Category
|
||||
}>()
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
|
||||
const origPrice = computed(() => props.variation.original_price || props.item.original_price)
|
||||
|
||||
const quotaLeftStr = computed(() => interpolate(STRINGS.quota_left, [props.variation.avail[1]]))
|
||||
|
||||
const variationLabelId = computed(() => `${store.htmlId}-variation-label-${props.item.id}-${props.variation.id}`)
|
||||
|
||||
const variationDescId = computed(() => `${store.htmlId}-variation-desc-${props.item.id}-${props.variation.id}`)
|
||||
|
||||
const variationPriceId = computed(() => `${store.htmlId}-variation-price-${props.item.id}-${props.variation.id}`)
|
||||
|
||||
const ariaLabelledby = computed(() => `${variationLabelId.value} ${variationPriceId.value}`)
|
||||
|
||||
const headingLevel = computed(() => props.category.name ? '5' : '4')
|
||||
|
||||
const showQuotaLeft = computed(() => props.variation.avail[1] !== null && props.variation.avail[0] === 100)
|
||||
|
||||
// TODO dedupe?
|
||||
const showPrices = computed(() => {
|
||||
// Determine if prices should be shown
|
||||
let hasPriced = false
|
||||
let cntItems = 0
|
||||
for (const cat of store.categories) {
|
||||
for (const item of cat.items) {
|
||||
if (item.has_variations) {
|
||||
cntItems += item.variations.length
|
||||
hasPriced = true
|
||||
} else {
|
||||
cntItems++
|
||||
hasPriced = hasPriced || item.price.gross !== '0.00' || item.free_price
|
||||
}
|
||||
}
|
||||
}
|
||||
return hasPriced || cntItems > 1
|
||||
})
|
||||
</script>
|
||||
<template lang="pug">
|
||||
.pretix-widget-variation(
|
||||
:data-id="variation.id",
|
||||
role="group",
|
||||
:aria-labelledby="ariaLabelledby",
|
||||
:aria-describedby="variationDescId"
|
||||
)
|
||||
.pretix-widget-item-row
|
||||
//- Variation description
|
||||
.pretix-widget-item-info-col
|
||||
.pretix-widget-item-title-and-description
|
||||
strong.pretix-widget-item-title(
|
||||
:id="variationLabelId",
|
||||
role="heading",
|
||||
:aria-level="headingLevel"
|
||||
) {{ variation.value }}
|
||||
.pretix-widget-item-description(
|
||||
v-if="variation.description",
|
||||
:id="variationDescId",
|
||||
v-html="variation.description"
|
||||
)
|
||||
p.pretix-widget-item-meta(v-if="showQuotaLeft")
|
||||
small {{ quotaLeftStr }}
|
||||
|
||||
//- Price
|
||||
.pretix-widget-item-price-col(:id="variationPriceId")
|
||||
PriceBox(
|
||||
v-if="showPrices",
|
||||
:price="variation.price",
|
||||
:freePrice="item.free_price",
|
||||
:originalPrice="origPrice",
|
||||
:mandatoryPricedAddons="item.mandatory_priced_addons",
|
||||
:suggestedPrice="variation.suggested_price",
|
||||
:fieldName="`price_${item.id}_${variation.id}`",
|
||||
:itemId="item.id"
|
||||
)
|
||||
span(v-if="!showPrices")
|
||||
|
||||
//- Availability
|
||||
.pretix-widget-item-availability-col
|
||||
AvailBox(:item="item", :variation="variation")
|
||||
|
||||
.pretix-widget-clear
|
||||
</template>
|
||||
<style lang="sass">
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref, onMounted, watch } from 'vue'
|
||||
import { StoreKey } from '~/sharedStore'
|
||||
import { STRINGS } from '~/i18n'
|
||||
import EventForm from './EventForm.vue'
|
||||
import EventList from './EventList.vue'
|
||||
import EventCalendar from './EventCalendar.vue'
|
||||
import EventWeekCalendar from './EventWeekCalendar.vue'
|
||||
import Overlay from './Overlay.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
mounted: []
|
||||
}>()
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
|
||||
const wrapper = ref<HTMLDivElement>()
|
||||
const formcomp = ref<InstanceType<typeof EventForm>>()
|
||||
const mobile = ref(false)
|
||||
|
||||
const classObject = computed(() => ({
|
||||
'pretix-widget': true,
|
||||
'pretix-widget-mobile': mobile.value,
|
||||
'pretix-widget-use-custom-spinners': true,
|
||||
}))
|
||||
|
||||
watch(mobile, (newValue) => {
|
||||
store.mobile = newValue
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (wrapper.value) {
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
mobile.value = entries[0].contentRect.width <= 800
|
||||
})
|
||||
resizeObserver.observe(wrapper.value)
|
||||
}
|
||||
|
||||
store.reload()
|
||||
emit('mounted')
|
||||
})
|
||||
|
||||
watch(() => store.view, (newValue, oldValue) => {
|
||||
if (oldValue && wrapper.value) {
|
||||
const rect = wrapper.value.getBoundingClientRect()
|
||||
if (rect.top < 0) {
|
||||
wrapper.value.scrollIntoView()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<template lang="pug">
|
||||
.pretix-widget-wrapper(ref="wrapper", tabindex="0", role="article", :aria-label="store.name")
|
||||
div(:class="classObject")
|
||||
.pretix-widget-loading(v-show="store.loading > 0")
|
||||
svg(width="128", height="128", viewBox="0 0 1792 1792", xmlns="http://www.w3.org/2000/svg")
|
||||
path.pretix-widget-primary-color(d="M1152 896q0-106-75-181t-181-75-181 75-75 181 75 181 181 75 181-75 75-181zm512-109v222q0 12-8 23t-20 13l-185 28q-19 54-39 91 35 50 107 138 10 12 10 25t-9 23q-27 37-99 108t-94 71q-12 0-26-9l-138-108q-44 23-91 38-16 136-29 186-7 28-36 28h-222q-14 0-24.5-8.5t-11.5-21.5l-28-184q-49-16-90-37l-141 107q-10 9-25 9-14 0-25-11-126-114-165-168-7-10-7-23 0-12 8-23 15-21 51-66.5t54-70.5q-27-50-41-99l-183-27q-13-2-21-12.5t-8-23.5v-222q0-12 8-23t19-13l186-28q14-46 39-92-40-57-107-138-10-12-10-24 0-10 9-23 26-36 98.5-107.5t94.5-71.5q13 0 26 10l138 107q44-23 91-38 16-136 29-186 7-28 36-28h222q14 0 24.5 8.5t11.5 21.5l28 184q49 16 90 37l142-107q9-9 24-9 13 0 25 10 129 119 165 170 7 8 7 22 0 12-8 23-15 21-51 66.5t-54 70.5q26 50 41 98l183 28q13 2 21 12.5t8 23.5z")
|
||||
|
||||
.pretix-widget-error-message(v-if="store.error && store.view !== 'event'") {{ store.error }}
|
||||
.pretix-widget-error-action(v-if="store.error && store.connectionError")
|
||||
a.pretix-widget-button(:href="store.newTabTarget", target="_blank") {{ STRINGS.open_new_tab }}
|
||||
|
||||
EventForm(v-if="store.view === 'event'", ref="formcomp")
|
||||
EventList(v-if="store.view === 'events'")
|
||||
EventCalendar(v-if="store.view === 'weeks'", :mobile="mobile")
|
||||
EventWeekCalendar(v-if="store.view === 'days'", :mobile="mobile")
|
||||
|
||||
.pretix-widget-clear
|
||||
.pretix-widget-attribution(v-if="store.poweredby", v-html="store.poweredby")
|
||||
|
||||
Overlay
|
||||
</template>
|
||||
<style lang="sass">
|
||||
</style>
|
||||
3
src/pretix/static/pretixpresale/widget/src/globals.d.ts
vendored
Normal file
3
src/pretix/static/pretixpresale/widget/src/globals.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
interface NamedNodeMap {
|
||||
[key: string]: Attr | undefined;
|
||||
}
|
||||
117
src/pretix/static/pretixpresale/widget/src/i18n.ts
Normal file
117
src/pretix/static/pretixpresale/widget/src/i18n.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
// Internationalization strings for the pretix widget
|
||||
// Django's i18n file expects `this` to be the global object, but ES modules
|
||||
// have `this` as undefined. Import as raw text and execute with a local context.
|
||||
|
||||
// TODO hack
|
||||
import djangoI18nScript from '../../../jsi18n/en/djangojs.js?raw'
|
||||
|
||||
interface Django {
|
||||
pgettext: (context: string, text: string) => string
|
||||
gettext: (text: string) => string
|
||||
interpolate: (fmt: string, obj: Record<string, unknown> | unknown[], named?: boolean) => string
|
||||
get_format: (formatType: string) => string | number
|
||||
}
|
||||
|
||||
// Create a local context object to capture django without polluting window
|
||||
const context: { django?: Django } = {}
|
||||
new Function(djangoI18nScript).call(context)
|
||||
const django = context.django!
|
||||
|
||||
export const STRINGS = {
|
||||
quantity: django.pgettext('widget', 'Quantity'),
|
||||
quantity_dec: django.pgettext('widget', 'Decrease quantity'),
|
||||
quantity_inc: django.pgettext('widget', 'Increase quantity'),
|
||||
filter_events_by: django.pgettext('widget', 'Filter events by'),
|
||||
filter: django.pgettext('widget', 'Filter'),
|
||||
price: django.pgettext('widget', 'Price'),
|
||||
original_price: django.pgettext('widget', 'Original price: %s'),
|
||||
new_price: django.pgettext('widget', 'New price: %s'),
|
||||
select: django.pgettext('widget', 'Select'),
|
||||
select_item: django.pgettext('widget', 'Select %s'),
|
||||
select_variant: django.pgettext('widget', 'Select variant %s'),
|
||||
sold_out: django.pgettext('widget', 'Sold out'),
|
||||
buy: django.pgettext('widget', 'Buy'),
|
||||
register: django.pgettext('widget', 'Register'),
|
||||
reserved: django.pgettext('widget', 'Reserved'),
|
||||
free: django.pgettext('widget', 'FREE'),
|
||||
price_from: django.pgettext('widget', 'from %(currency)s %(price)s'),
|
||||
image_of: django.pgettext('widget', 'Image of %s'),
|
||||
tax_incl: django.pgettext('widget', 'incl. %(rate)s% %(taxname)s'),
|
||||
tax_plus: django.pgettext('widget', 'plus %(rate)s% %(taxname)s'),
|
||||
tax_incl_mixed: django.pgettext('widget', 'incl. taxes'),
|
||||
tax_plus_mixed: django.pgettext('widget', 'plus taxes'),
|
||||
quota_left: django.pgettext('widget', 'currently available: %s'),
|
||||
unavailable_require_voucher: django.pgettext('widget', 'Only available with a voucher'),
|
||||
unavailable_available_from: django.pgettext('widget', 'Not yet available'),
|
||||
unavailable_available_until: django.pgettext('widget', 'Not available anymore'),
|
||||
unavailable_active: django.pgettext('widget', 'Currently not available'),
|
||||
unavailable_hidden_if_item_available: django.pgettext('widget', 'Not yet available'),
|
||||
order_min: django.pgettext('widget', 'minimum amount to order: %s'),
|
||||
exit: django.pgettext('widget', 'Close ticket shop'),
|
||||
loading_error: django.pgettext('widget', 'The ticket shop could not be loaded.'),
|
||||
loading_error_429: django.pgettext('widget', 'There are currently a lot of users in this ticket shop. Please open the shop in a new tab to continue.'),
|
||||
open_new_tab: django.pgettext('widget', 'Open ticket shop'),
|
||||
checkout: django.pgettext('widget', 'Checkout'),
|
||||
cart_error: django.pgettext('widget', 'The cart could not be created. Please try again later'),
|
||||
cart_error_429: django.pgettext('widget', 'We could not create your cart, since there are currently too many users in this ticket shop. Please click "Continue" to retry in a new tab.'),
|
||||
waiting_list: django.pgettext('widget', 'Waiting list'),
|
||||
cart_exists: django.pgettext('widget', 'You currently have an active cart for this event. If you select more products, they will be added to your existing cart.'),
|
||||
resume_checkout: django.pgettext('widget', 'Resume checkout'),
|
||||
redeem_voucher: django.pgettext('widget', 'Redeem a voucher'),
|
||||
redeem: django.pgettext('widget', 'Redeem'),
|
||||
voucher_code: django.pgettext('widget', 'Voucher code'),
|
||||
close: django.pgettext('widget', 'Close'),
|
||||
close_checkout: django.pgettext('widget', 'Close checkout'),
|
||||
cancel_blocked: django.pgettext('widget', 'You cannot cancel this operation. Please wait for loading to finish.'),
|
||||
continue: django.pgettext('widget', 'Continue'),
|
||||
variations: django.pgettext('widget', 'Show variants'),
|
||||
hide_variations: django.pgettext('widget', 'Hide variants'),
|
||||
back_to_list: django.pgettext('widget', 'Choose a different event'),
|
||||
back_to_dates: django.pgettext('widget', 'Choose a different date'),
|
||||
back: django.pgettext('widget', 'Back'),
|
||||
next_month: django.pgettext('widget', 'Next month'),
|
||||
previous_month: django.pgettext('widget', 'Previous month'),
|
||||
next_week: django.pgettext('widget', 'Next week'),
|
||||
previous_week: django.pgettext('widget', 'Previous week'),
|
||||
show_seating: django.pgettext('widget', 'Open seat selection'),
|
||||
seating_plan_waiting_list: django.pgettext('widget', 'Some or all ticket categories are currently sold out. If you want, you can add yourself to the waiting list. We will then notify if seats are available again.'),
|
||||
load_more: django.pgettext('widget', 'Load more'),
|
||||
days: {
|
||||
MO: django.gettext('Mo'),
|
||||
TU: django.gettext('Tu'),
|
||||
WE: django.gettext('We'),
|
||||
TH: django.gettext('Th'),
|
||||
FR: django.gettext('Fr'),
|
||||
SA: django.gettext('Sa'),
|
||||
SU: django.gettext('Su'),
|
||||
MONDAY: django.gettext('Monday'),
|
||||
TUESDAY: django.gettext('Tuesday'),
|
||||
WEDNESDAY: django.gettext('Wednesday'),
|
||||
THURSDAY: django.gettext('Thursday'),
|
||||
FRIDAY: django.gettext('Friday'),
|
||||
SATURDAY: django.gettext('Saturday'),
|
||||
SUNDAY: django.gettext('Sunday'),
|
||||
},
|
||||
months: {
|
||||
'01': django.gettext('January'),
|
||||
'02': django.gettext('February'),
|
||||
'03': django.gettext('March'),
|
||||
'04': django.gettext('April'),
|
||||
'05': django.gettext('May'),
|
||||
'06': django.gettext('June'),
|
||||
'07': django.gettext('July'),
|
||||
'08': django.gettext('August'),
|
||||
'09': django.gettext('September'),
|
||||
10: django.gettext('October'),
|
||||
11: django.gettext('November'),
|
||||
12: django.gettext('December'),
|
||||
} as Record<string, string>,
|
||||
} as const
|
||||
|
||||
export function interpolate (fmt: string, obj: Record<string, unknown> | unknown[], named = false): string {
|
||||
return django.interpolate(fmt, obj, named)
|
||||
}
|
||||
|
||||
export function getFormat (formatType: string): string | number {
|
||||
return django.get_format(formatType)
|
||||
}
|
||||
80
src/pretix/static/pretixpresale/widget/src/lib/store.ts
Normal file
80
src/pretix/static/pretixpresale/widget/src/lib/store.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { reactive, computed, watch } from 'vue'
|
||||
import type { WatchCallback, WatchOptions, UnwrapNestedRefs } from 'vue'
|
||||
|
||||
interface StoreMethods {
|
||||
$reset: () => void
|
||||
$watch: <T>(source: () => T, callback: WatchCallback<T>, options?: WatchOptions) => void
|
||||
}
|
||||
|
||||
type GetterReturnTypes<G> = {
|
||||
readonly [K in keyof G]: G[K] extends () => infer R ? R : never
|
||||
}
|
||||
|
||||
type Store<S, G, A> = UnwrapNestedRefs<S> & GetterReturnTypes<G> & A & StoreMethods
|
||||
type GettersTree<S> = Record<string, (this: S, state: S) => any> | Record<string, () => any>
|
||||
type ActionsTree = Record<string, (...args: any[]) => any>
|
||||
|
||||
export function createStore<
|
||||
S extends object,
|
||||
G extends GettersTree<S>,
|
||||
A extends ActionsTree
|
||||
> (
|
||||
// name: string,
|
||||
config: {
|
||||
state: () => S
|
||||
getters?: G & ThisType<UnwrapNestedRefs<S> & GetterReturnTypes<G> & A & StoreMethods>
|
||||
actions?: A & ThisType<UnwrapNestedRefs<S> & GetterReturnTypes<G> & A & StoreMethods>
|
||||
}
|
||||
): Store<S, G, A> {
|
||||
type StoreType = Store<S, G, A>
|
||||
const store = reactive(config.state()) as StoreType
|
||||
|
||||
// Add getters as computed properties
|
||||
if (config.getters) {
|
||||
for (const key of Object.keys(config.getters) as (keyof G)[]) {
|
||||
const getter = config.getters[key]
|
||||
const computedRef = computed(() => (getter as () => unknown).call(store))
|
||||
Object.defineProperty(store, key, {
|
||||
get: () => computedRef.value,
|
||||
enumerable: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Add actions bound to the store
|
||||
if (config.actions) {
|
||||
for (const key of Object.keys(config.actions) as (keyof A)[]) {
|
||||
const action = config.actions[key]
|
||||
;(store as Record<string, unknown>)[key as string] = (action as (...args: unknown[]) => unknown).bind(store)
|
||||
}
|
||||
}
|
||||
|
||||
store.$reset = function () {
|
||||
const cleanState = config.state()
|
||||
const cleanKeys = new Set([
|
||||
'$reset',
|
||||
'$watch',
|
||||
...Object.keys(cleanState),
|
||||
...Object.keys(config.getters ?? {}),
|
||||
...Object.keys(config.actions ?? {})
|
||||
])
|
||||
|
||||
// Delete any keys that aren't in clean state and aren't known non-state keys
|
||||
for (const key of Object.keys(store)) {
|
||||
if (!cleanKeys.has(key)) {
|
||||
delete (store as Record<string, unknown>)[key]
|
||||
}
|
||||
}
|
||||
|
||||
// Set all state values from clean state
|
||||
for (const key of Object.keys(cleanState) as (keyof S)[]) {
|
||||
;(store as S)[key] = cleanState[key]
|
||||
}
|
||||
}
|
||||
|
||||
store.$watch = function <T>(source: () => T, callback: WatchCallback<T>, options?: WatchOptions) {
|
||||
watch(source.bind(store), callback.bind(store), options)
|
||||
}
|
||||
|
||||
return store
|
||||
}
|
||||
130
src/pretix/static/pretixpresale/widget/src/main.ts
Normal file
130
src/pretix/static/pretixpresale/widget/src/main.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { createApp, nextTick, type App } from 'vue'
|
||||
import { createWidgetInstance } from '~/widget'
|
||||
import { createButtonInstance } from '~/button'
|
||||
import { createWidgetStore, StoreKey } from '~/sharedStore'
|
||||
import ButtonComponent from '~/components/Button.vue'
|
||||
import { docReady, makeid } from '~/utils'
|
||||
import type { WidgetData } from '~/types'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
PretixWidget: PretixWidgetAPI
|
||||
pretixWidgetCallback?: () => void
|
||||
}
|
||||
}
|
||||
|
||||
interface PretixWidgetAPI {
|
||||
build_widgets: boolean
|
||||
widget_data: WidgetData
|
||||
buildWidgets: () => void
|
||||
open: (
|
||||
targetUrl: string,
|
||||
voucher?: string | null,
|
||||
subevent?: string | number | null,
|
||||
items?: { item: string; count: string }[],
|
||||
widgetData?: Record<string, string>,
|
||||
skipSslCheck?: boolean,
|
||||
disableIframe?: boolean
|
||||
) => void
|
||||
addLoadListener: (callback: () => void) => void
|
||||
addCloseListener: (callback: () => void) => void
|
||||
_loaded: Array<() => void>
|
||||
_closed: Array<() => void>
|
||||
}
|
||||
|
||||
const widgetlist: App[] = []
|
||||
const buttonlist: App[] = []
|
||||
|
||||
window.PretixWidget = {
|
||||
build_widgets: true,
|
||||
widget_data: { referer: location.href },
|
||||
// TODO move somewhere else and rename?
|
||||
_loaded: [],
|
||||
_closed: [],
|
||||
buildWidgets,
|
||||
open: openWidget,
|
||||
addLoadListener (f) { this._loaded.push(f) },
|
||||
addCloseListener (f) { this._closed.push(f) },
|
||||
}
|
||||
|
||||
async function buildWidgets () {
|
||||
// TODO what does this do?
|
||||
document.createElement('pretix-widget')
|
||||
document.createElement('pretix-button')
|
||||
|
||||
await docReady()
|
||||
const widgetElements = document.querySelectorAll('pretix-widget, div.pretix-widget-compat')
|
||||
for (const [i, el] of Array.from(widgetElements).entries()) {
|
||||
widgetlist.push(createWidgetInstance(el, el.id || `pretix-widget-${i}`))
|
||||
}
|
||||
const buttonElements = document.querySelectorAll('pretix-button, div.pretix-button-compat')
|
||||
for (const [i, el] of Array.from(buttonElements).entries()) {
|
||||
buttonlist.push(createButtonInstance(el, el.id || `pretix-button-${i}`))
|
||||
}
|
||||
}
|
||||
|
||||
function openWidget (
|
||||
targetUrl: string,
|
||||
voucher?: string | null,
|
||||
subevent?: string | number | null,
|
||||
items?: { item: string; count: string }[],
|
||||
widgetData?: Record<string, string>,
|
||||
skipSslCheck?: boolean,
|
||||
disableIframe?: boolean
|
||||
): void {
|
||||
if (!targetUrl.match(/\/$/)) {
|
||||
targetUrl += '/'
|
||||
}
|
||||
|
||||
const allWidgetData: WidgetData = JSON.parse(JSON.stringify(window.PretixWidget.widget_data))
|
||||
if (widgetData) {
|
||||
Object.assign(allWidgetData, widgetData)
|
||||
}
|
||||
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
root.classList.add('pretix-widget-hidden')
|
||||
|
||||
const store = createWidgetStore({
|
||||
targetUrl,
|
||||
voucher: voucher ?? null,
|
||||
subevent: subevent ?? null,
|
||||
skipSsl: skipSslCheck ?? false,
|
||||
disableIframe: disableIframe ?? false,
|
||||
widgetData: allWidgetData,
|
||||
htmlId: makeid(16),
|
||||
isButton: true,
|
||||
buttonItems: items ?? [],
|
||||
buttonText: '',
|
||||
})
|
||||
|
||||
const app = createApp(ButtonComponent)
|
||||
app.provide(StoreKey, store)
|
||||
app.config.errorHandler = (error, _vm, info) => {
|
||||
console.error('[pretix-widget-open]', info, error)
|
||||
}
|
||||
app.mount(root)
|
||||
|
||||
nextTick(() => {
|
||||
if (store.useIframe) {
|
||||
const form = root.querySelector('form') as HTMLFormElement
|
||||
if (form) {
|
||||
const formData = new FormData(form)
|
||||
store.buy(formData)
|
||||
}
|
||||
} else {
|
||||
const form = root.querySelector('form') as HTMLFormElement
|
||||
if (form) form.submit()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof window.pretixWidgetCallback !== 'undefined') {
|
||||
window.pretixWidgetCallback()
|
||||
}
|
||||
|
||||
if (window.PretixWidget.build_widgets) {
|
||||
window.PretixWidget.buildWidgets()
|
||||
}
|
||||
|
||||
// TODO debug exposes
|
||||
519
src/pretix/static/pretixpresale/widget/src/sharedStore.ts
Normal file
519
src/pretix/static/pretixpresale/widget/src/sharedStore.ts
Normal file
@@ -0,0 +1,519 @@
|
||||
import { nextTick, type InjectionKey } from 'vue'
|
||||
import { createStore } from '~/lib/store'
|
||||
import { fetchProductList, submitCart, checkAsyncTask, ApiError } from '~/api'
|
||||
import type { CartResponse } from '~/api'
|
||||
import { STRINGS } from '~/i18n'
|
||||
import { setCookie, getCookie, makeid } from '~/utils'
|
||||
import type { Category, DayEntry, EventEntry, LightboxState, MetaFilterField, WidgetData } from '~/types'
|
||||
|
||||
export const globalWidgetId = makeid(16)
|
||||
|
||||
export type WidgetStore = ReturnType<typeof createWidgetStore>
|
||||
export const StoreKey: InjectionKey<WidgetStore> = Symbol('WidgetStore')
|
||||
|
||||
export function createWidgetStore (config: {
|
||||
targetUrl: string
|
||||
isButton?: boolean
|
||||
voucher?: string | null
|
||||
subevent?: string | number | null
|
||||
listType?: string | null
|
||||
skipSsl?: boolean
|
||||
disableIframe?: boolean
|
||||
disableVouchers?: boolean
|
||||
disableFilters?: boolean
|
||||
displayEventInfo?: boolean | null
|
||||
filter?: string | null
|
||||
items?: string | null
|
||||
categories?: string | null
|
||||
variations?: string | null
|
||||
widgetData: WidgetData
|
||||
htmlId: string
|
||||
// Button-specific
|
||||
buttonItems?: { item: string; count: string }[]
|
||||
buttonText?: string
|
||||
}) {
|
||||
return createStore({
|
||||
state: () => ({
|
||||
// Target/URL state
|
||||
targetUrl: config.targetUrl,
|
||||
parentStack: [] as string[],
|
||||
subevent: config.subevent ?? null as string | number | null,
|
||||
|
||||
// Configuration
|
||||
voucherCode: config.voucher ?? null as string | null,
|
||||
skipSsl: config.skipSsl ?? false,
|
||||
disableIframe: config.disableIframe ?? false,
|
||||
disableVouchers: config.disableVouchers ?? false,
|
||||
disableFilters: config.disableFilters ?? false,
|
||||
displayEventInfo: config.displayEventInfo ?? null as boolean | null,
|
||||
filter: config.filter ?? null as string | null,
|
||||
itemFilter: config.items ?? null as string | null,
|
||||
categoryFilter: config.categories ?? null as string | null,
|
||||
variationFilter: config.variations ?? null as string | null,
|
||||
style: config.listType ?? null as string | null,
|
||||
widgetData: config.widgetData,
|
||||
widgetId: `pretix-widget-${globalWidgetId}`,
|
||||
htmlId: config.htmlId,
|
||||
|
||||
// View state
|
||||
view: null as 'event' | 'events' | 'weeks' | 'days' | null,
|
||||
loading: config.isButton ? 0 : 1,
|
||||
error: null as string | null,
|
||||
connectionError: false,
|
||||
frameDismissed: false,
|
||||
|
||||
// Event data
|
||||
name: null as string | null,
|
||||
dateRange: null as string | null,
|
||||
location: null as string | null,
|
||||
frontpageText: null as string | null,
|
||||
categories: [] as Category[],
|
||||
currency: '',
|
||||
displayNetPrices: false,
|
||||
voucherExplanationText: null as string | null,
|
||||
displayAddToCart: false,
|
||||
waitingListEnabled: false,
|
||||
showVariationsExpanded: !!config.variations,
|
||||
cartId: null as string | null,
|
||||
cartExists: false,
|
||||
vouchersExist: false,
|
||||
hasSeatingPlan: false,
|
||||
hasSeatingPlanWaitinglist: false,
|
||||
itemnum: 0,
|
||||
poweredby: '',
|
||||
|
||||
// Calendar/list data
|
||||
events: null as EventEntry[] | null,
|
||||
weeks: null as DayEntry[][] | null,
|
||||
days: null as DayEntry[] | null,
|
||||
date: null as string | null,
|
||||
week: null as [number, number] | null,
|
||||
hasMoreEvents: false,
|
||||
offset: 0,
|
||||
appendEvents: false,
|
||||
metaFilterFields: [] as MetaFilterField[],
|
||||
|
||||
// UI state
|
||||
mobile: false,
|
||||
|
||||
// Button-specific
|
||||
isButton: config.isButton ?? false,
|
||||
items: (config.buttonItems ?? []) as { item: string; count: string }[],
|
||||
buttonText: config.buttonText ?? '',
|
||||
|
||||
// Overlay (always initialized, no null guards)
|
||||
overlay: {
|
||||
frameSrc: '',
|
||||
frameLoading: false,
|
||||
frameShown: false,
|
||||
errorMessage: null as string | null,
|
||||
errorUrlAfter: null as string | null,
|
||||
errorUrlAfterNewTab: false,
|
||||
lightbox: null as LightboxState | null,
|
||||
},
|
||||
|
||||
// Async task state
|
||||
asyncTaskId: null as string | null,
|
||||
asyncTaskCheckUrl: null as string | null,
|
||||
asyncTaskTimeout: null as ReturnType<typeof setTimeout> | null,
|
||||
asyncTaskInterval: 100,
|
||||
voucher: null as string | null,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
useIframe (): boolean {
|
||||
if ((window as any).crossOriginIsolated === true) return false
|
||||
return !this.disableIframe && (this.skipSsl || /https.*/.test(document.location.protocol))
|
||||
},
|
||||
|
||||
cookieName (): string {
|
||||
return `pretix_widget_${this.targetUrl.replace(/[^a-zA-Z0-9]+/g, '_')}`
|
||||
},
|
||||
|
||||
cartIdFromCookie (): string | null {
|
||||
return getCookie(this.cookieName) ?? null
|
||||
},
|
||||
|
||||
consentParameter (): string {
|
||||
if (this.widgetData.consent) {
|
||||
return `&consent=${encodeURIComponent(this.widgetData.consent)}`
|
||||
}
|
||||
return ''
|
||||
},
|
||||
|
||||
additionalURLParams (): string {
|
||||
if (!window.location.search.includes('utm_')) {
|
||||
return ''
|
||||
}
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
for (const [key] of params.entries()) {
|
||||
if (!key.startsWith('utm_')) {
|
||||
params.delete(key)
|
||||
}
|
||||
}
|
||||
return params.toString()
|
||||
},
|
||||
|
||||
newTabTarget (): string {
|
||||
return this.subevent ? `${this.targetUrl}${this.subevent}/` : this.targetUrl
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
triggerLoadCallback () {
|
||||
nextTick(() => {
|
||||
for (const callback of (window as any).PretixWidget._loaded || []) {
|
||||
callback()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async reload (opt: { focus?: string } = {}) {
|
||||
if (this.isButton) return
|
||||
|
||||
let url: string
|
||||
if (this.subevent) {
|
||||
url = `${this.targetUrl}${this.subevent}/widget/product_list?lang=${LANG}`
|
||||
} else {
|
||||
url = `${this.targetUrl}widget/product_list?lang=${LANG}`
|
||||
}
|
||||
|
||||
if (this.offset) url += `&offset=${this.offset}`
|
||||
if (this.filter) url += `&${this.filter}`
|
||||
if (this.itemFilter) url += `&items=${encodeURIComponent(this.itemFilter)}`
|
||||
if (this.categoryFilter) url += `&categories=${encodeURIComponent(this.categoryFilter)}`
|
||||
if (this.variationFilter) url += `&variations=${encodeURIComponent(this.variationFilter)}`
|
||||
if (this.voucherCode) url += `&voucher=${encodeURIComponent(this.voucherCode)}`
|
||||
|
||||
const cartIdCookie = this.cartIdFromCookie
|
||||
if (cartIdCookie) url += `&cart_id=${encodeURIComponent(cartIdCookie)}`
|
||||
if (this.date !== null) {
|
||||
url += `&date=${this.date.substring(0, 7)}`
|
||||
} else if (this.week !== null) {
|
||||
url += `&date=${this.week[0]}-W${this.week[1]}`
|
||||
}
|
||||
if (this.style !== null) url += `&style=${encodeURIComponent(this.style)}`
|
||||
|
||||
try {
|
||||
const { data, responseUrl } = await fetchProductList(url)
|
||||
|
||||
// Check for redirect
|
||||
const newUrl = responseUrl.substring(0, responseUrl.indexOf('/widget/product_list?') + 1)
|
||||
const oldUrl = url.substring(0, url.indexOf('/widget/product_list?') + 1)
|
||||
if (newUrl !== oldUrl) {
|
||||
let adjustedUrl = newUrl
|
||||
if (this.subevent) {
|
||||
adjustedUrl = adjustedUrl.substring(0, adjustedUrl.lastIndexOf('/', adjustedUrl.length - 1) + 1)
|
||||
}
|
||||
this.targetUrl = adjustedUrl
|
||||
this.reload()
|
||||
return
|
||||
}
|
||||
|
||||
this.connectionError = false
|
||||
|
||||
if (data.weeks !== undefined) {
|
||||
this.weeks = data.weeks
|
||||
this.date = data.date ?? null
|
||||
this.week = null
|
||||
this.events = null
|
||||
this.view = 'weeks'
|
||||
this.name = data.name ?? null
|
||||
this.frontpageText = data.frontpage_text ?? null
|
||||
this.metaFilterFields = data.meta_filter_fields ?? []
|
||||
} else if (data.days !== undefined) {
|
||||
this.days = data.days
|
||||
this.date = null
|
||||
this.week = data.week ?? null
|
||||
this.events = null
|
||||
this.view = 'days'
|
||||
this.name = data.name ?? null
|
||||
this.frontpageText = data.frontpage_text ?? null
|
||||
this.metaFilterFields = data.meta_filter_fields ?? []
|
||||
} else if (data.events !== undefined) {
|
||||
this.events = this.appendEvents && this.events
|
||||
? this.events.concat(data.events)
|
||||
: data.events
|
||||
this.appendEvents = false
|
||||
this.weeks = null
|
||||
this.view = 'events'
|
||||
this.name = data.name ?? null
|
||||
this.frontpageText = data.frontpage_text ?? null
|
||||
this.hasMoreEvents = data.has_more_events ?? false
|
||||
this.metaFilterFields = data.meta_filter_fields ?? []
|
||||
} else {
|
||||
this.view = 'event'
|
||||
this.targetUrl = data.target_url ?? this.targetUrl
|
||||
this.subevent = data.subevent ?? null
|
||||
this.name = data.name ?? null
|
||||
this.frontpageText = data.frontpage_text ?? null
|
||||
this.dateRange = data.date_range ?? null
|
||||
this.location = data.location ?? null
|
||||
this.categories = data.items_by_category ?? []
|
||||
this.currency = data.currency ?? ''
|
||||
this.displayNetPrices = data.display_net_prices ?? false
|
||||
this.voucherExplanationText = data.voucher_explanation_text ?? null
|
||||
this.error = data.error ?? null
|
||||
this.displayAddToCart = data.display_add_to_cart ?? false
|
||||
this.waitingListEnabled = data.waiting_list_enabled ?? false
|
||||
this.showVariationsExpanded = data.show_variations_expanded || !!this.variationFilter
|
||||
this.cartId = cartIdCookie
|
||||
this.cartExists = data.cart_exists ?? false
|
||||
this.vouchersExist = data.vouchers_exist ?? false
|
||||
this.hasSeatingPlan = data.has_seating_plan ?? false
|
||||
this.hasSeatingPlanWaitinglist = data.has_seating_plan_waitinglist ?? false
|
||||
this.itemnum = data.itemnum ?? 0
|
||||
}
|
||||
|
||||
this.poweredby = data.poweredby ?? ''
|
||||
|
||||
if (this.loading > 0) {
|
||||
this.loading--
|
||||
this.triggerLoadCallback()
|
||||
}
|
||||
|
||||
// Auto-open seating plan if applicable
|
||||
if (
|
||||
this.parentStack.length > 0
|
||||
&& this.hasSeatingPlan
|
||||
&& this.categories.length === 0
|
||||
&& !this.frameDismissed
|
||||
&& this.useIframe
|
||||
&& !this.error
|
||||
&& !this.hasSeatingPlanWaitinglist
|
||||
) {
|
||||
this.startseating()
|
||||
} else if (opt.focus) {
|
||||
nextTick(() => {
|
||||
document.querySelector<HTMLElement>(opt.focus!)?.focus()
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
this.categories = []
|
||||
this.currency = ''
|
||||
if (e instanceof ApiError && e.status === 429) {
|
||||
this.error = STRINGS.loading_error_429
|
||||
} else {
|
||||
this.error = STRINGS.loading_error
|
||||
}
|
||||
this.connectionError = true
|
||||
if (this.loading > 0) {
|
||||
this.loading--
|
||||
this.triggerLoadCallback()
|
||||
}
|
||||
}
|
||||
},
|
||||
getFormAction (): string {
|
||||
if (!this.useIframe && this.isButton && this.items.length === 0) {
|
||||
if (this.voucherCode) return `${this.targetUrl}redeem`
|
||||
if (this.subevent) return `${this.targetUrl}${this.subevent}/`
|
||||
return this.targetUrl
|
||||
}
|
||||
|
||||
let checkoutUrl = `/${this.targetUrl.replace(/^[^/]+:\/\/([^/]+)\//, '')}w/${globalWidgetId}/`
|
||||
if (!this.cartExists) {
|
||||
checkoutUrl += 'checkout/start'
|
||||
}
|
||||
if (this.additionalURLParams) {
|
||||
checkoutUrl += `?${this.additionalURLParams}`
|
||||
}
|
||||
|
||||
let formTarget = `${this.targetUrl}w/${globalWidgetId}/cart/add?iframe=1&next=${encodeURIComponent(checkoutUrl)}`
|
||||
if (this.cartIdFromCookie) {
|
||||
formTarget += `&take_cart_id=${this.cartIdFromCookie}`
|
||||
}
|
||||
formTarget += this.consentParameter
|
||||
return formTarget
|
||||
},
|
||||
getVoucherFormTarget (): string {
|
||||
let formTarget = `${this.targetUrl}w/${globalWidgetId}/redeem?iframe=1&locale=${LANG}`
|
||||
if (this.cartIdFromCookie) {
|
||||
formTarget += `&take_cart_id=${this.cartIdFromCookie}`
|
||||
}
|
||||
if (this.subevent) {
|
||||
formTarget += `&subevent=${this.subevent}`
|
||||
}
|
||||
if (this.widgetData) {
|
||||
formTarget += `&widget_data=${encodeURIComponent(JSON.stringify(this.widgetData))}`
|
||||
}
|
||||
formTarget += this.consentParameter
|
||||
if (this.additionalURLParams) {
|
||||
formTarget += `&${this.additionalURLParams}`
|
||||
}
|
||||
return formTarget
|
||||
},
|
||||
handleCartResponse (data: CartResponse) {
|
||||
if (data.redirect) {
|
||||
if (data.cart_id) {
|
||||
this.cartId = data.cart_id
|
||||
setCookie(this.cookieName, data.cart_id, 30)
|
||||
}
|
||||
|
||||
let redirectUrl = data.redirect
|
||||
if (redirectUrl.substring(0, 1) === '/') {
|
||||
redirectUrl = `${this.targetUrl.replace(/^([^/]+:\/\/[^/]+)\/.*$/, '$1')}${redirectUrl}`
|
||||
}
|
||||
|
||||
let url = redirectUrl
|
||||
if (url.includes('?')) {
|
||||
url = `${url}&iframe=1&locale=${LANG}&take_cart_id=${this.cartId}`
|
||||
} else {
|
||||
url = `${url}?iframe=1&locale=${LANG}&take_cart_id=${this.cartId}`
|
||||
}
|
||||
url += this.consentParameter
|
||||
if (this.additionalURLParams) {
|
||||
url += `&${this.additionalURLParams}`
|
||||
}
|
||||
|
||||
if (data.success === false) {
|
||||
url = url.replace(/checkout\/start/g, '')
|
||||
this.overlay.errorMessage = data.message ?? null
|
||||
if (data.has_cart) {
|
||||
this.overlay.errorUrlAfter = url
|
||||
}
|
||||
this.overlay.frameLoading = false
|
||||
} else {
|
||||
this.overlay.frameSrc = url
|
||||
}
|
||||
} else if (data.async_id && data.check_url) {
|
||||
this.asyncTaskId = data.async_id
|
||||
this.asyncTaskCheckUrl = `${this.targetUrl.replace(/^([^/]+:\/\/[^/]+)\/.*$/, '$1')}${data.check_url}`
|
||||
this.asyncTaskTimeout = window.setTimeout(() => this.pollAsyncTask(), this.asyncTaskInterval)
|
||||
this.asyncTaskInterval = 250
|
||||
}
|
||||
},
|
||||
async pollAsyncTask () {
|
||||
if (!this.asyncTaskCheckUrl) return
|
||||
try {
|
||||
const data = await checkAsyncTask(this.asyncTaskCheckUrl)
|
||||
this.handleCartResponse(data)
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError && (e.status === 200 || (e.status >= 400 && e.status < 500))) {
|
||||
this.overlay.errorMessage = STRINGS.cart_error
|
||||
this.overlay.frameLoading = false
|
||||
} else {
|
||||
this.asyncTaskTimeout = window.setTimeout(() => this.pollAsyncTask(), 1000)
|
||||
}
|
||||
}
|
||||
},
|
||||
async buy (formData: FormData, event?: Event) {
|
||||
if (this.useIframe) {
|
||||
if (event) event.preventDefault()
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.isButton && this.items.length === 0) {
|
||||
if (this.voucherCode) {
|
||||
this.voucherOpen(this.voucherCode)
|
||||
} else {
|
||||
this.resume()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const url = `${this.getFormAction()}&locale=${LANG}&ajax=1`
|
||||
this.overlay.frameLoading = true
|
||||
this.asyncTaskInterval = 100
|
||||
|
||||
try {
|
||||
const data = await submitCart(url, formData)
|
||||
this.handleCartResponse(data)
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) {
|
||||
if (e.status === 429) {
|
||||
this.overlay.errorMessage = STRINGS.cart_error_429
|
||||
this.overlay.frameLoading = false
|
||||
this.overlay.errorUrlAfter = this.newTabTarget
|
||||
this.overlay.errorUrlAfterNewTab = true
|
||||
} else if (e.status === 405) {
|
||||
this.targetUrl = e.responseUrl.substring(0, e.responseUrl.indexOf('/cart/add') - 18)
|
||||
this.overlay.frameLoading = false
|
||||
} else {
|
||||
this.overlay.errorMessage = STRINGS.cart_error
|
||||
this.overlay.frameLoading = false
|
||||
}
|
||||
} else {
|
||||
this.overlay.errorMessage = STRINGS.cart_error
|
||||
this.overlay.frameLoading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
redeem (voucherCode: string, event?: Event) {
|
||||
if (this.useIframe) {
|
||||
if (event) event.preventDefault()
|
||||
this.voucherOpen(voucherCode)
|
||||
}
|
||||
},
|
||||
voucherOpen (voucherCode: string) {
|
||||
const redirectUrl = `${this.getVoucherFormTarget()}&voucher=${encodeURIComponent(voucherCode)}`
|
||||
if (this.useIframe) {
|
||||
this.overlay.frameSrc = redirectUrl
|
||||
} else {
|
||||
window.open(redirectUrl)
|
||||
}
|
||||
},
|
||||
resume () {
|
||||
let redirectUrl = `${this.targetUrl}w/${globalWidgetId}/`
|
||||
if (this.subevent && !this.cartId) {
|
||||
redirectUrl += `${this.subevent}/`
|
||||
}
|
||||
redirectUrl += `?iframe=1&locale=${LANG}`
|
||||
if (this.cartId) {
|
||||
redirectUrl += `&take_cart_id=${this.cartId}`
|
||||
}
|
||||
if (this.widgetData) {
|
||||
redirectUrl += `&widget_data=${encodeURIComponent(JSON.stringify(this.widgetData))}`
|
||||
}
|
||||
redirectUrl += this.consentParameter
|
||||
if (this.additionalURLParams) {
|
||||
redirectUrl += `&${this.additionalURLParams}`
|
||||
}
|
||||
if (this.useIframe) {
|
||||
this.overlay.frameSrc = redirectUrl
|
||||
} else {
|
||||
window.open(redirectUrl)
|
||||
}
|
||||
},
|
||||
startwaiting () {
|
||||
let redirectUrl = `${this.targetUrl}w/${globalWidgetId}/waitinglist/?iframe=1&locale=${LANG}`
|
||||
if (this.subevent) {
|
||||
redirectUrl += `&subevent=${this.subevent}`
|
||||
}
|
||||
if (this.additionalURLParams) {
|
||||
redirectUrl += `&${this.additionalURLParams}`
|
||||
}
|
||||
if (this.useIframe) {
|
||||
this.overlay.frameSrc = redirectUrl
|
||||
} else {
|
||||
window.open(redirectUrl)
|
||||
}
|
||||
},
|
||||
startseating () {
|
||||
let redirectUrl = `${this.targetUrl}w/${globalWidgetId}`
|
||||
if (this.subevent) {
|
||||
redirectUrl += `/${this.subevent}`
|
||||
}
|
||||
redirectUrl += `/seatingframe/?iframe=1&locale=${LANG}`
|
||||
if (this.voucherCode) {
|
||||
redirectUrl += `&voucher=${encodeURIComponent(this.voucherCode)}`
|
||||
}
|
||||
if (this.cartId) {
|
||||
redirectUrl += `&take_cart_id=${this.cartId}`
|
||||
}
|
||||
if (this.widgetData) {
|
||||
redirectUrl += `&widget_data=${encodeURIComponent(JSON.stringify(this.widgetData))}`
|
||||
}
|
||||
redirectUrl += this.consentParameter
|
||||
if (this.additionalURLParams) {
|
||||
redirectUrl += `&${this.additionalURLParams}`
|
||||
}
|
||||
if (this.useIframe) {
|
||||
this.overlay.frameSrc = redirectUrl
|
||||
} else {
|
||||
window.open(redirectUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
92
src/pretix/static/pretixpresale/widget/src/types.ts
Normal file
92
src/pretix/static/pretixpresale/widget/src/types.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
// Domain model types for the pretix widget
|
||||
|
||||
export interface Price {
|
||||
gross: string
|
||||
net: string
|
||||
rate: string
|
||||
name: string
|
||||
includes_mixed_tax_rate?: boolean
|
||||
}
|
||||
|
||||
export interface Availability {
|
||||
color: 'green' | 'orange' | 'red'
|
||||
text?: string
|
||||
reason?: string
|
||||
}
|
||||
|
||||
export interface Variation {
|
||||
id: number
|
||||
value: string
|
||||
description?: string
|
||||
price: Price
|
||||
suggested_price?: Price
|
||||
original_price?: string
|
||||
avail: [number, number | null]
|
||||
order_max: number
|
||||
current_unavailability_reason?: string
|
||||
allow_waitinglist?: boolean
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
id: number
|
||||
name: string
|
||||
description?: string
|
||||
picture?: string
|
||||
picture_fullsize?: string
|
||||
price: Price
|
||||
suggested_price?: Price
|
||||
original_price?: string
|
||||
avail: [number, number | null]
|
||||
order_min?: number
|
||||
order_max: number
|
||||
has_variations: boolean
|
||||
variations: Variation[]
|
||||
free_price: boolean
|
||||
min_price?: string
|
||||
max_price?: string
|
||||
mandatory_priced_addons?: boolean
|
||||
current_unavailability_reason?: string
|
||||
allow_waitinglist?: boolean
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
id: number
|
||||
name?: string
|
||||
description?: string
|
||||
items: Item[]
|
||||
}
|
||||
|
||||
export interface EventEntry {
|
||||
name: string
|
||||
event_url: string
|
||||
subevent?: number | string
|
||||
date_range: string
|
||||
location: string
|
||||
time?: string
|
||||
continued?: boolean
|
||||
availability: Availability
|
||||
}
|
||||
|
||||
export interface DayEntry {
|
||||
date: string
|
||||
day_formatted: string
|
||||
events: EventEntry[]
|
||||
}
|
||||
|
||||
export interface MetaFilterField {
|
||||
key: string
|
||||
label: string
|
||||
choices: [string, string][]
|
||||
}
|
||||
|
||||
export interface LightboxState {
|
||||
image: string
|
||||
description: string
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
export interface WidgetData {
|
||||
referer: string
|
||||
consent?: string
|
||||
[key: string]: string | undefined
|
||||
}
|
||||
100
src/pretix/static/pretixpresale/widget/src/utils.ts
Normal file
100
src/pretix/static/pretixpresale/widget/src/utils.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
// Utility functions for the pretix widget
|
||||
|
||||
import { getFormat } from '~/i18n'
|
||||
|
||||
// Cookie utilities
|
||||
export function setCookie (cname: string, cvalue: string, exdays: number): void {
|
||||
const d = new Date()
|
||||
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000)
|
||||
const expires = `expires=${d.toUTCString()}`
|
||||
document.cookie = `${cname}=${cvalue};${expires};path=/`
|
||||
}
|
||||
|
||||
export function getCookie (name: string): string | null {
|
||||
const value = `; ${document.cookie}`
|
||||
const parts = value.split(`; ${name}=`)
|
||||
if (parts.length === 2) {
|
||||
return parts.pop()?.split(';').shift() || null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Number formatting
|
||||
export function roundTo (n: number, digits = 0): number {
|
||||
const multiplicator = Math.pow(10, digits)
|
||||
n = parseFloat((n * multiplicator).toFixed(11))
|
||||
return Math.round(n) / multiplicator
|
||||
}
|
||||
|
||||
// TODO use https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat instead?
|
||||
export function floatformat (val: number | string, places = 2): string {
|
||||
if (typeof val === 'string') {
|
||||
val = parseFloat(val)
|
||||
}
|
||||
const parts = roundTo(val, places).toFixed(places).split('.')
|
||||
if (places === 0) {
|
||||
return parts[0]
|
||||
}
|
||||
const grouping = getFormat('NUMBER_GROUPING') as number
|
||||
const thousandSep = getFormat('THOUSAND_SEPARATOR') as string
|
||||
const decimalSep = getFormat('DECIMAL_SEPARATOR') as string
|
||||
parts[0] = parts[0].replace(
|
||||
new RegExp(`\\B(?=(\\d{${grouping}})+(?!\\d))`, 'g'),
|
||||
thousandSep
|
||||
)
|
||||
return `${parts[0]}${decimalSep}${parts[1]}`
|
||||
}
|
||||
|
||||
export function autofloatformat (val: number | string, places = 2): string {
|
||||
const numVal = typeof val === 'string' ? parseFloat(val) : val
|
||||
if (numVal === roundTo(numVal, 0)) {
|
||||
places = 0
|
||||
}
|
||||
return floatformat(numVal, places)
|
||||
}
|
||||
|
||||
// String/number utilities
|
||||
export function padNumber (number: number, size = 2): string {
|
||||
let s = String(number)
|
||||
while (s.length < size) {
|
||||
s = `0${s}`
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
export function getISOWeeks (year: number): number {
|
||||
const d = new Date(year, 0, 1)
|
||||
const isLeap = new Date(year, 1, 29).getMonth() === 1
|
||||
// Check for a Jan 1 that's a Thursday or a leap year that has a Wednesday Jan 1
|
||||
return d.getDay() === 4 || (isLeap && d.getDay() === 3) ? 53 : 52
|
||||
}
|
||||
|
||||
export function makeid (length: number): string {
|
||||
let text = ''
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||
for (let i = 0; i < length; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length))
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
export function siteIsSecure (): boolean {
|
||||
return /https.*/.test(document.location.protocol)
|
||||
}
|
||||
|
||||
// HTML utility
|
||||
export function stripHTML (s: string): string {
|
||||
const div = document.createElement('div')
|
||||
div.innerHTML = s
|
||||
return div.textContent || div.innerText || ''
|
||||
}
|
||||
|
||||
// docReady - DOM ready detection (returns a Promise)
|
||||
export function docReady (): Promise<void> {
|
||||
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
document.addEventListener('DOMContentLoaded', () => resolve(), { once: true })
|
||||
})
|
||||
}
|
||||
1
src/pretix/static/pretixpresale/widget/src/vite-env.d.ts
vendored
Normal file
1
src/pretix/static/pretixpresale/widget/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare const LANG: string
|
||||
69
src/pretix/static/pretixpresale/widget/src/widget.ts
Normal file
69
src/pretix/static/pretixpresale/widget/src/widget.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { createApp, type App } from 'vue'
|
||||
import WidgetComponent from '~/components/Widget.vue'
|
||||
import { createWidgetStore, StoreKey } from '~/sharedStore'
|
||||
import { makeid } from '~/utils'
|
||||
import type { WidgetData } from '~/types'
|
||||
|
||||
export function createWidgetInstance (element: Element, htmlId?: string): App {
|
||||
let targetUrl = element.attributes.event.value
|
||||
if (!targetUrl.match(/\/$/)) {
|
||||
targetUrl += '/'
|
||||
}
|
||||
|
||||
const displayEventInfoAttr = element.attributes['display-event-info']?.value
|
||||
// null means "auto" (as before), everything other than "false" is true
|
||||
const displayEventInfo: boolean | null
|
||||
= 'display-event-info' in element.attributes && displayEventInfoAttr !== 'auto' ? displayEventInfoAttr !== 'false' : null
|
||||
|
||||
const widgetData: WidgetData = JSON.parse(JSON.stringify(window.PretixWidget.widget_data))
|
||||
for (const attr of Array.from(element.attributes)) {
|
||||
if (attr.name.match(/^data-.*$/)) {
|
||||
widgetData[attr.name.replace(/^data-/, '')] = attr.value
|
||||
}
|
||||
}
|
||||
|
||||
const store = createWidgetStore({
|
||||
targetUrl,
|
||||
voucher: element.attributes.voucher?.value || null,
|
||||
subevent: element.attributes.subevent?.value || null,
|
||||
listType: element.attributes['list-type']?.value || element.attributes.style?.value || null,
|
||||
skipSsl: 'skip-ssl-check' in element.attributes,
|
||||
disableIframe: 'disable-iframe' in element.attributes,
|
||||
disableVouchers: 'disable-vouchers' in element.attributes,
|
||||
disableFilters: 'disable-filters' in element.attributes,
|
||||
displayEventInfo,
|
||||
filter: element.attributes.filter?.value || null,
|
||||
items: element.attributes.items?.value || null,
|
||||
categories: element.attributes.categories?.value || null,
|
||||
variations: element.attributes.variations?.value || null,
|
||||
widgetData,
|
||||
htmlId: htmlId || element.id || makeid(16),
|
||||
})
|
||||
|
||||
const observer = new MutationObserver((mutationList) => {
|
||||
for (const mutation of mutationList) {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName?.startsWith('data-')) {
|
||||
const attrName = mutation.attributeName.substring(5)
|
||||
const attrValue = (mutation.target as Element).getAttribute(mutation.attributeName)
|
||||
store.widgetData[attrName] = attrValue
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// TODO I don't think we need this anymore in vue3
|
||||
// if (element.tagName !== 'pretix-widget') {
|
||||
// element.innerHTML = '<pretix-widget></pretix-widget>'
|
||||
// // we need to watch the container as well as the replaced root-node (see mounted())
|
||||
// observer.observe(element, observerOptions)
|
||||
// }
|
||||
|
||||
const app = createApp(WidgetComponent)
|
||||
app.provide(StoreKey, store)
|
||||
app.config.errorHandler = (error, _vm, info) => {
|
||||
console.error('[pretix-widget]', info, error)
|
||||
}
|
||||
app.mount(element)
|
||||
observer.observe(element, { attributes: true })
|
||||
|
||||
return app
|
||||
}
|
||||
11
src/pretix/static/pretixpresale/widget/tsconfig.json
Normal file
11
src/pretix/static/pretixpresale/widget/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../../../../tsconfig.json",
|
||||
"include": ["src/**/*"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
},
|
||||
"types": ["node", "vite/client"]
|
||||
}
|
||||
}
|
||||
33
src/pretix/static/pretixpresale/widget/vite.config.ts
Normal file
33
src/pretix/static/pretixpresale/widget/vite.config.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue()
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'~': import.meta.dirname + '/src',
|
||||
},
|
||||
},
|
||||
build: {
|
||||
manifest: true,
|
||||
outDir: import.meta.dirname + '/../../../../../static.dist/vite/widget',
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: import.meta.dirname + '/src/main.ts',
|
||||
},
|
||||
output: {
|
||||
format: 'iife',
|
||||
entryFileNames: 'widget.js',
|
||||
assetFileNames: 'widget.[ext]',
|
||||
},
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['moment', 'jquery']
|
||||
},
|
||||
define: {
|
||||
LANG: JSON.stringify(process.env.PRETIX_WIDGET_LANG || 'en')
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user