mirror of
https://github.com/pretix/pretix.git
synced 2026-05-09 15:54:03 +00:00
migrate webcheckin plugin to vite+vue3
- migrate vue sfcs to script setup and pug - move fetch calls into a api.ts module - move common formatting and i18n strings into module
This commit is contained in:
@@ -0,0 +1,271 @@
|
|||||||
|
import type { I18nString, SubEvent } from './i18n'
|
||||||
|
|
||||||
|
const settingsEl = document.getElementById('api-settings')
|
||||||
|
const { urls } = JSON.parse(settingsEl.textContent || '{}') as { urls: {
|
||||||
|
lists: string
|
||||||
|
questions: string
|
||||||
|
} }
|
||||||
|
|
||||||
|
// interfaces generated from api docs
|
||||||
|
export interface PaginatedResponse<T> {
|
||||||
|
count: number
|
||||||
|
next: string | null
|
||||||
|
previous: string | null
|
||||||
|
results: T[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckinList {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
all_products: boolean
|
||||||
|
limit_products: number[]
|
||||||
|
subevent: SubEvent | null
|
||||||
|
position_count?: number
|
||||||
|
checkin_count?: number
|
||||||
|
include_pending: boolean
|
||||||
|
allow_multiple_entries: boolean
|
||||||
|
allow_entry_after_exit: boolean
|
||||||
|
rules: Record<string, unknown>
|
||||||
|
exit_all_at: string | null
|
||||||
|
addon_match: boolean
|
||||||
|
ignore_in_statistics?: boolean
|
||||||
|
consider_tickets_used?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Checkin {
|
||||||
|
id: number
|
||||||
|
list: number
|
||||||
|
datetime: string
|
||||||
|
type: 'entry' | 'exit'
|
||||||
|
gate: number | null
|
||||||
|
device: number | null
|
||||||
|
device_id: number | null
|
||||||
|
auto_checked_in: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Seat {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
zone_name: string
|
||||||
|
row_name: string
|
||||||
|
row_label: string | null
|
||||||
|
seat_number: string
|
||||||
|
seat_label: string | null
|
||||||
|
seat_guid: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Position {
|
||||||
|
id: number
|
||||||
|
order: string
|
||||||
|
positionid: number
|
||||||
|
canceled?: boolean
|
||||||
|
item: { id?: number; name: I18nString; internal_name?: string; admission?: boolean }
|
||||||
|
variation: { id?: number; value: I18nString } | null
|
||||||
|
price: string
|
||||||
|
attendee_name: string
|
||||||
|
attendee_name_parts: Record<string, string>
|
||||||
|
attendee_email: string | null
|
||||||
|
company?: string | null
|
||||||
|
street?: string | null
|
||||||
|
zipcode?: string | null
|
||||||
|
city?: string | null
|
||||||
|
country?: string | null
|
||||||
|
state?: string | null
|
||||||
|
voucher?: number | null
|
||||||
|
voucher_budget_use?: string | null
|
||||||
|
tax_rate: string
|
||||||
|
tax_value: string
|
||||||
|
tax_code?: string | null
|
||||||
|
tax_rule: number | null
|
||||||
|
secret: string
|
||||||
|
addon_to: number | null
|
||||||
|
subevent: SubEvent | null
|
||||||
|
discount?: number | null
|
||||||
|
blocked: string[] | null
|
||||||
|
valid_from: string | null
|
||||||
|
valid_until: string | null
|
||||||
|
pseudonymization_id: string
|
||||||
|
seat: Seat | null
|
||||||
|
checkins: Checkin[]
|
||||||
|
downloads?: { output: string; url: string }[]
|
||||||
|
answers: Answer[]
|
||||||
|
pdf_data?: Record<string, unknown>
|
||||||
|
plugin_data?: Record<string, unknown>
|
||||||
|
// Additional fields from checkin list positions endpoint
|
||||||
|
order__status?: string
|
||||||
|
order__valid_if_pending?: boolean
|
||||||
|
order__require_approval?: boolean
|
||||||
|
order__locale?: string
|
||||||
|
require_attention?: boolean
|
||||||
|
addons?: Addon[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Answer {
|
||||||
|
question: number | AnswerQuestion
|
||||||
|
answer: string
|
||||||
|
question_identifier: string
|
||||||
|
options: number[]
|
||||||
|
option_identifiers: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnswerQuestion {
|
||||||
|
id: number
|
||||||
|
question: I18nString
|
||||||
|
help_text?: I18nString
|
||||||
|
type: string
|
||||||
|
required: boolean
|
||||||
|
position: number
|
||||||
|
items: number[]
|
||||||
|
identifier: string
|
||||||
|
ask_during_checkin: boolean
|
||||||
|
show_during_checkin: boolean
|
||||||
|
hidden?: boolean
|
||||||
|
print_on_invoice?: boolean
|
||||||
|
options: QuestionOption[]
|
||||||
|
valid_number_min?: string | null
|
||||||
|
valid_number_max?: string | null
|
||||||
|
valid_date_min?: string | null
|
||||||
|
valid_date_max?: string | null
|
||||||
|
valid_datetime_min?: string | null
|
||||||
|
valid_datetime_max?: string | null
|
||||||
|
valid_file_portrait?: boolean
|
||||||
|
valid_string_length_max?: number | null
|
||||||
|
dependency_question?: number | null
|
||||||
|
dependency_values?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuestionOption {
|
||||||
|
id: number
|
||||||
|
identifier: string
|
||||||
|
position: number
|
||||||
|
answer: I18nString
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Addon {
|
||||||
|
item: { name: I18nString; internal_name?: string }
|
||||||
|
variation: { value: I18nString } | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckinStatusVariation {
|
||||||
|
id: number
|
||||||
|
value: string
|
||||||
|
checkin_count: number
|
||||||
|
position_count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckinStatusItem {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
checkin_count: number
|
||||||
|
admission: boolean
|
||||||
|
position_count: number
|
||||||
|
variations: CheckinStatusVariation[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckinStatus {
|
||||||
|
checkin_count: number
|
||||||
|
position_count: number
|
||||||
|
inside_count: number
|
||||||
|
event?: { name: string }
|
||||||
|
items?: CheckinStatusItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RedeemRequest {
|
||||||
|
questions_supported: boolean
|
||||||
|
canceled_supported: boolean
|
||||||
|
ignore_unpaid: boolean
|
||||||
|
type: 'entry' | 'exit'
|
||||||
|
answers: Record<string, string>
|
||||||
|
datetime?: string | null
|
||||||
|
force?: boolean
|
||||||
|
nonce?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RedeemResponseList {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
event: string
|
||||||
|
subevent: number | null
|
||||||
|
include_pending: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RedeemResponse {
|
||||||
|
status: 'ok' | 'error' | 'incomplete'
|
||||||
|
reason?: 'invalid' | 'unpaid' | 'blocked' | 'invalid_time' | 'canceled' | 'already_redeemed' | 'product' | 'rules' | 'ambiguous' | 'revoked' | 'unapproved' | 'error'
|
||||||
|
reason_explanation?: string | null
|
||||||
|
position?: Position
|
||||||
|
questions?: AnswerQuestion[]
|
||||||
|
checkin_texts?: string[]
|
||||||
|
require_attention?: boolean
|
||||||
|
list?: RedeemResponseList
|
||||||
|
}
|
||||||
|
|
||||||
|
const CSRF_TOKEN = document.querySelector<HTMLInputElement>('input[name=csrfmiddlewaretoken]')?.value ?? ''
|
||||||
|
|
||||||
|
function handleAuthError (response: Response): void {
|
||||||
|
if ([401, 403].includes(response.status)) {
|
||||||
|
window.location.href = '/control/login?next=' + encodeURIComponent(
|
||||||
|
window.location.pathname + window.location.search + window.location.hash
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
// generic fetch wrapper, not sure if this should be exposed
|
||||||
|
async fetch <T> (url: string, options?: RequestInit): Promise<T> {
|
||||||
|
const response = await fetch(url, options)
|
||||||
|
handleAuthError(response)
|
||||||
|
if (!response.ok && response.status !== 400 && response.status !== 404) {
|
||||||
|
throw new Error('HTTP status ' + response.status)
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
},
|
||||||
|
async fetchCheckinLists (endsAfter?: string): Promise<PaginatedResponse<CheckinList>> {
|
||||||
|
const cutoff = endsAfter ?? moment().subtract(8, 'hours').toISOString()
|
||||||
|
const url = `${urls.lists}?exclude=checkin_count&exclude=position_count&expand=subevent&ends_after=${cutoff}`
|
||||||
|
return api.fetch(url)
|
||||||
|
},
|
||||||
|
async fetchCheckinList (listId: string): Promise<CheckinList> {
|
||||||
|
return api.fetch(`${urls.lists}${listId}/?expand=subevent`)
|
||||||
|
},
|
||||||
|
async fetchNextPage<T> (nextUrl: string): Promise<PaginatedResponse<T>> {
|
||||||
|
return api.fetch(nextUrl)
|
||||||
|
},
|
||||||
|
async fetchStatus (listId: number): Promise<CheckinStatus> {
|
||||||
|
return api.fetch(`${urls.lists}${listId}/status/`)
|
||||||
|
},
|
||||||
|
async searchPositions (listId: number, query: string): Promise<PaginatedResponse<Position>> {
|
||||||
|
const url = `${urls.lists}${listId}/positions/?ignore_status=true&expand=subevent&expand=item&expand=variation&check_rules=true&search=${encodeURIComponent(query)}`
|
||||||
|
return api.fetch(url)
|
||||||
|
},
|
||||||
|
async redeemPosition (
|
||||||
|
listId: number,
|
||||||
|
positionId: string,
|
||||||
|
data: RedeemRequest,
|
||||||
|
untrusted: boolean = false
|
||||||
|
): Promise<RedeemResponse> {
|
||||||
|
let url = `${urls.lists}${listId}/positions/${encodeURIComponent(positionId)}/redeem/?expand=item&expand=subevent&expand=variation&expand=answers.question&expand=addons`
|
||||||
|
if (untrusted) url += '&untrusted_input=true'
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': CSRF_TOKEN,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
|
||||||
|
handleAuthError(response)
|
||||||
|
|
||||||
|
if (response.status === 404) {
|
||||||
|
return { status: 'error', reason: 'invalid' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok && response.status !== 400) {
|
||||||
|
throw new Error('HTTP status ' + response.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,28 +1,21 @@
|
|||||||
<template>
|
<script setup lang="ts">
|
||||||
<a class="list-group-item" href="#" @click.prevent="$emit('selected', list)">
|
import { computed } from 'vue'
|
||||||
<div class="row">
|
import type { CheckinList } from '../api'
|
||||||
<div class="col-md-6">
|
import { formatSubevent } from '../i18n'
|
||||||
{{ list.name }}
|
|
||||||
</div>
|
const props = defineProps<{
|
||||||
<div class="col-md-6 text-muted">
|
list: CheckinList
|
||||||
{{ subevent }}
|
}>()
|
||||||
</div>
|
|
||||||
</div>
|
defineEmits<{
|
||||||
</a>
|
selected: [list: CheckinList]
|
||||||
</template>
|
}>()
|
||||||
<script>
|
|
||||||
export default {
|
const subevent = computed(() => formatSubevent(props.list.subevent))
|
||||||
components: {},
|
|
||||||
props: {
|
|
||||||
list: Object
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
subevent () {
|
|
||||||
if (!this.list.subevent) return '';
|
|
||||||
const name = i18nstring_localize(this.list.subevent.name)
|
|
||||||
const date = moment.utc(this.list.subevent.date_from).tz(this.$root.timezone).format(this.$root.datetime_format)
|
|
||||||
return `${name} · ${date}`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
<template lang="pug">
|
||||||
|
a.list-group-item(href="#", @click.prevent="$emit('selected', list)")
|
||||||
|
.row
|
||||||
|
.col-md-6 {{ list.name }}
|
||||||
|
.col-md-6.text-muted {{ subevent }}
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -1,101 +1,99 @@
|
|||||||
<template>
|
<script setup lang="ts">
|
||||||
<div class="panel panel-primary checkinlist-select">
|
import { ref, onMounted } from 'vue'
|
||||||
<div class="panel-heading">
|
import { api } from '../api'
|
||||||
<h3 class="panel-title">
|
import type { CheckinList } from '../api'
|
||||||
{{ $root.strings['checkinlist.select'] }}
|
import { STRINGS } from '../i18n'
|
||||||
</h3>
|
import CheckinlistItem from './checkinlist-item.vue'
|
||||||
</div>
|
|
||||||
<ul class="list-group">
|
const emit = defineEmits<{
|
||||||
<checkinlist-item v-if="lists" v-for="l in lists" :list="l" :key="l.id" @selected="$emit('selected', l)"></checkinlist-item>
|
selected: [list: CheckinList]
|
||||||
<li v-if="loading" class="list-group-item text-center">
|
}>()
|
||||||
<span class="fa fa-4x fa-cog fa-spin loading-icon"></span>
|
|
||||||
</li>
|
const loading = ref(false)
|
||||||
<li v-else-if="error" class="list-group-item text-center">
|
const error = ref<unknown>(null)
|
||||||
{{ error }}
|
const lists = ref<CheckinList[] | null>(null)
|
||||||
</li>
|
const nextUrl = ref<string | null>(null)
|
||||||
<a v-else-if="next_url" class="list-group-item text-center" href="#" @click.prevent="loadNext">
|
|
||||||
{{ $root.strings['pagination.next'] }}
|
async function load () {
|
||||||
</a>
|
loading.value = true
|
||||||
</ul>
|
error.value = null
|
||||||
</div>
|
|
||||||
</template>
|
try {
|
||||||
<script>
|
if (location.hash) {
|
||||||
export default {
|
const listId = location.hash.substring(1)
|
||||||
components: {
|
try {
|
||||||
CheckinlistItem: CheckinlistItem.default,
|
const data = await api.fetchCheckinList(listId)
|
||||||
},
|
loading.value = false
|
||||||
data() {
|
if (data.id) {
|
||||||
return {
|
emit('selected', data)
|
||||||
loading: false,
|
load()
|
||||||
error: null,
|
}
|
||||||
lists: null,
|
} catch {
|
||||||
next_url: null,
|
location.hash = ''
|
||||||
}
|
load()
|
||||||
},
|
}
|
||||||
// TODO: pagination
|
return
|
||||||
mounted() {
|
}
|
||||||
this.load()
|
|
||||||
},
|
const data = await api.fetchCheckinLists()
|
||||||
methods: {
|
loading.value = false
|
||||||
load() {
|
|
||||||
this.loading = true
|
if (data.results) {
|
||||||
const cutoff = moment().subtract(8, 'hours').toISOString()
|
lists.value = data.results
|
||||||
if (location.hash) {
|
nextUrl.value = data.next
|
||||||
fetch(this.$root.api.lists + location.hash.substr(1) + '/' + '?expand=subevent')
|
} else if (data.results === 0) {
|
||||||
.then(response => response.json())
|
error.value = STRINGS['checkinlist.none']
|
||||||
.then(data => {
|
} else {
|
||||||
this.loading = false
|
error.value = data
|
||||||
if (data.id) {
|
}
|
||||||
this.$emit('selected', data)
|
} catch (e) {
|
||||||
} else {
|
loading.value = false
|
||||||
location.hash = ''
|
error.value = e
|
||||||
this.load()
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reason => {
|
|
||||||
location.hash = ''
|
|
||||||
this.load()
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fetch(this.$root.api.lists + '?exclude=checkin_count&exclude=position_count&expand=subevent&ends_after=' + cutoff)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
this.loading = false
|
|
||||||
if (data.results) {
|
|
||||||
this.lists = data.results
|
|
||||||
this.next_url = data.next
|
|
||||||
} else if (data.results === 0) {
|
|
||||||
this.error = this.$root.strings['checkinlist.none']
|
|
||||||
} else {
|
|
||||||
this.error = data
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reason => {
|
|
||||||
this.loading = false
|
|
||||||
this.error = reason
|
|
||||||
})
|
|
||||||
},
|
|
||||||
loadNext() {
|
|
||||||
this.loading = true
|
|
||||||
fetch(this.next_url)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
this.loading = false
|
|
||||||
if (data.results) {
|
|
||||||
this.lists.push(...data.results)
|
|
||||||
this.next_url = data.next
|
|
||||||
} else if (data.results === 0) {
|
|
||||||
this.error = this.$root.strings['checkinlist.none']
|
|
||||||
} else {
|
|
||||||
this.error = data
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reason => {
|
|
||||||
this.loading = false
|
|
||||||
this.error = reason
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadNext () {
|
||||||
|
if (!nextUrl.value) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await api.fetchNextPage<CheckinList>(nextUrl.value)
|
||||||
|
loading.value = false
|
||||||
|
|
||||||
|
if (data.results) {
|
||||||
|
lists.value.push(...data.results)
|
||||||
|
nextUrl.value = data.next
|
||||||
|
} else if (data.results === 0) {
|
||||||
|
error.value = STRINGS['checkinlist.none']
|
||||||
|
} else {
|
||||||
|
error.value = data
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
loading.value = false
|
||||||
|
error.value = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
load()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
<template lang="pug">
|
||||||
|
.panel.panel-primary.checkinlist-select
|
||||||
|
.panel-heading
|
||||||
|
h3.panel-title {{ STRINGS['checkinlist.select'] }}
|
||||||
|
ul.list-group
|
||||||
|
CheckinlistItem(
|
||||||
|
v-for="l in lists",
|
||||||
|
:key="l.id",
|
||||||
|
:list="l",
|
||||||
|
@selected="emit('selected', $event)"
|
||||||
|
)
|
||||||
|
li.list-group-item.text-center(v-if="loading")
|
||||||
|
span.fa.fa-4x.fa-cog.fa-spin.loading-icon
|
||||||
|
li.list-group-item.text-center(v-else-if="error") {{ error }}
|
||||||
|
a.list-group-item.text-center(v-else-if="nextUrl", href="#", @click.prevent="loadNext")
|
||||||
|
| {{ STRINGS['pagination.next'] }}
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -1,54 +1,64 @@
|
|||||||
<template>
|
<script setup lang="ts">
|
||||||
<input class="form-control" :required="required">
|
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||||
</template>
|
import { dateFormat, datetimeLocale } from '../i18n'
|
||||||
<script>
|
|
||||||
export default {
|
const props = defineProps<{
|
||||||
props: ["required", "value"],
|
required?: boolean
|
||||||
mounted: function () {
|
modelValue?: string
|
||||||
var vm = this;
|
id?: string
|
||||||
var multiple = this.multiple;
|
}>()
|
||||||
$(this.$el)
|
|
||||||
.datetimepicker(this.opts())
|
const emit = defineEmits<{
|
||||||
.trigger("change")
|
'update:modelValue': [value: string]
|
||||||
.on("dp.change", function (e) {
|
}>()
|
||||||
vm.$emit("input", $(this).data('DateTimePicker').date().format("YYYY-MM-DD"));
|
|
||||||
});
|
const input = ref<HTMLInputElement>()
|
||||||
if (!vm.value) {
|
|
||||||
$(this.$el).data("DateTimePicker").viewDate(moment().hour(0).minute(0).second(0).millisecond(0));
|
const opts = {
|
||||||
} else {
|
format: dateFormat,
|
||||||
$(this.$el).data("DateTimePicker").date(moment(vm.value));
|
locale: datetimeLocale,
|
||||||
}
|
useCurrent: false,
|
||||||
},
|
showClear: props.required,
|
||||||
methods: {
|
icons: {
|
||||||
opts: function () {
|
time: 'fa fa-clock-o',
|
||||||
return {
|
date: 'fa fa-calendar',
|
||||||
format: $("body").attr("data-dateformat"),
|
up: 'fa fa-chevron-up',
|
||||||
locale: $("body").attr("data-datetimelocale"),
|
down: 'fa fa-chevron-down',
|
||||||
useCurrent: false,
|
previous: 'fa fa-chevron-left',
|
||||||
showClear: this.required,
|
next: 'fa fa-chevron-right',
|
||||||
icons: {
|
today: 'fa fa-screenshot',
|
||||||
time: 'fa fa-clock-o',
|
clear: 'fa fa-trash',
|
||||||
date: 'fa fa-calendar',
|
close: 'fa fa-remove',
|
||||||
up: 'fa fa-chevron-up',
|
},
|
||||||
down: 'fa fa-chevron-down',
|
|
||||||
previous: 'fa fa-chevron-left',
|
|
||||||
next: 'fa fa-chevron-right',
|
|
||||||
today: 'fa fa-screenshot',
|
|
||||||
clear: 'fa fa-trash',
|
|
||||||
close: 'fa fa-remove'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
value: function (val) {
|
|
||||||
$(this.$el).data('DateTimePicker').date(moment(val));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
destroyed: function () {
|
|
||||||
$(this.$el)
|
|
||||||
.off()
|
|
||||||
.datetimepicker("destroy");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
if (val) {
|
||||||
|
$(input.value!).data('DateTimePicker').date(moment(val))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
$(input.value!)
|
||||||
|
.datetimepicker(opts)
|
||||||
|
.trigger('change')
|
||||||
|
.on('dp.change', function (this: HTMLElement) {
|
||||||
|
emit('update:modelValue', $(this).data('DateTimePicker').date().format('YYYY-MM-DD'))
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!props.modelValue) {
|
||||||
|
$(input.value!).data('DateTimePicker').viewDate(moment().hour(0).minute(0).second(0).millisecond(0))
|
||||||
|
} else {
|
||||||
|
$(input.value!).data('DateTimePicker').date(moment(props.modelValue))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
$(input.value!)
|
||||||
|
.off()
|
||||||
|
.datetimepicker('destroy')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template lang="pug">
|
||||||
|
input.form-control(ref="input", :id="id", :required="required")
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -1,55 +1,65 @@
|
|||||||
<template>
|
<script setup lang="ts">
|
||||||
<input class="form-control" :required="required">
|
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||||
</template>
|
import { datetimeFormat, datetimeLocale, timezone } from '../i18n'
|
||||||
<script>
|
|
||||||
export default {
|
const props = defineProps<{
|
||||||
props: ["required", "value"],
|
required?: boolean
|
||||||
mounted: function () {
|
modelValue?: string
|
||||||
var vm = this;
|
id?: string
|
||||||
var multiple = this.multiple;
|
}>()
|
||||||
$(this.$el)
|
|
||||||
.datetimepicker(this.opts())
|
const emit = defineEmits<{
|
||||||
.trigger("change")
|
'update:modelValue': [value: string]
|
||||||
.on("dp.change", function (e) {
|
}>()
|
||||||
vm.$emit("input", $(this).data('DateTimePicker').date().toISOString());
|
|
||||||
});
|
const input = ref<HTMLInputElement>()
|
||||||
if (!vm.value) {
|
|
||||||
$(this.$el).data("DateTimePicker").viewDate(moment().hour(0).minute(0).second(0).millisecond(0));
|
const opts = {
|
||||||
} else {
|
format: datetimeFormat,
|
||||||
$(this.$el).data("DateTimePicker").date(moment(vm.value));
|
locale: datetimeLocale,
|
||||||
}
|
timeZone: timezone,
|
||||||
},
|
useCurrent: false,
|
||||||
methods: {
|
showClear: props.required,
|
||||||
opts: function () {
|
icons: {
|
||||||
return {
|
time: 'fa fa-clock-o',
|
||||||
format: $("body").attr("data-datetimeformat"),
|
date: 'fa fa-calendar',
|
||||||
locale: $("body").attr("data-datetimelocale"),
|
up: 'fa fa-chevron-up',
|
||||||
timeZone: $("body").attr("data-timezone"),
|
down: 'fa fa-chevron-down',
|
||||||
useCurrent: false,
|
previous: 'fa fa-chevron-left',
|
||||||
showClear: this.required,
|
next: 'fa fa-chevron-right',
|
||||||
icons: {
|
today: 'fa fa-screenshot',
|
||||||
time: 'fa fa-clock-o',
|
clear: 'fa fa-trash',
|
||||||
date: 'fa fa-calendar',
|
close: 'fa fa-remove',
|
||||||
up: 'fa fa-chevron-up',
|
},
|
||||||
down: 'fa fa-chevron-down',
|
|
||||||
previous: 'fa fa-chevron-left',
|
|
||||||
next: 'fa fa-chevron-right',
|
|
||||||
today: 'fa fa-screenshot',
|
|
||||||
clear: 'fa fa-trash',
|
|
||||||
close: 'fa fa-remove'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
value: function (val) {
|
|
||||||
$(this.$el).data('DateTimePicker').date(moment(val));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
destroyed: function () {
|
|
||||||
$(this.$el)
|
|
||||||
.off()
|
|
||||||
.datetimepicker("destroy");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
if (val) {
|
||||||
|
$(input.value!).data('DateTimePicker').date(moment(val))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
$(input.value!)
|
||||||
|
.datetimepicker(opts)
|
||||||
|
.trigger('change')
|
||||||
|
.on('dp.change', function (this: HTMLElement) {
|
||||||
|
emit('update:modelValue', $(this).data('DateTimePicker').date().toISOString())
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!props.modelValue) {
|
||||||
|
$(input.value!).data('DateTimePicker').viewDate(moment().hour(0).minute(0).second(0).millisecond(0))
|
||||||
|
} else {
|
||||||
|
$(input.value!).data('DateTimePicker').date(moment(props.modelValue))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
$(input.value!)
|
||||||
|
.off()
|
||||||
|
.datetimepicker('destroy')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template lang="pug">
|
||||||
|
input.form-control(ref="input", :id="id", :required="required")
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -1,48 +1,48 @@
|
|||||||
<template>
|
<script setup lang="ts">
|
||||||
<a class="list-group-item searchresult" href="#" @click.prevent="$emit('selected', position)" ref="a">
|
import { computed, ref } from 'vue'
|
||||||
<div class="details">
|
import type { Position } from '../api'
|
||||||
<h4>{{ position.order }}-{{ position.positionid }} {{ position.attendee_name }}</h4>
|
import { STRINGS, i18nstringLocalize, formatSubevent } from '../i18n'
|
||||||
<span>{{ itemvar }}<br></span>
|
|
||||||
<span v-if="subevent">{{ subevent }}<br></span>
|
const props = defineProps<{
|
||||||
<div class="secret">{{ position.secret }}</div>
|
position: Position
|
||||||
</div>
|
}>()
|
||||||
<div :class="`status status-${status}`">
|
|
||||||
<span v-if="position.require_attention"><span class="fa fa-warning"></span><br></span>
|
defineEmits<{
|
||||||
{{ $root.strings[`status.${status}`] }}
|
selected: [position: Position]
|
||||||
</div>
|
}>()
|
||||||
</a>
|
|
||||||
</template>
|
const rootEl = ref<HTMLAnchorElement>()
|
||||||
<script>
|
|
||||||
export default {
|
const status = computed(() => {
|
||||||
components: {},
|
if (props.position.checkins.length) return 'redeemed'
|
||||||
props: {
|
if (props.position.order__status === 'n' && props.position.order__valid_if_pending) return 'pending_valid'
|
||||||
position: Object
|
if (props.position.order__status === 'n' && props.position.order__require_approval) return 'require_approval'
|
||||||
},
|
return props.position.order__status
|
||||||
computed: {
|
})
|
||||||
status() {
|
|
||||||
if (this.position.checkins.length) return 'redeemed';
|
const itemvar = computed(() => {
|
||||||
if (this.position.order__status === 'n' && this.position.order__valid_if_pending) return 'pending_valid';
|
if (props.position.variation) {
|
||||||
if (this.position.order__status === 'n' && this.position.order__require_approval) return 'require_approval';
|
return `${i18nstringLocalize(props.position.item.name)} – ${i18nstringLocalize(props.position.variation.value)}`
|
||||||
return this.position.order__status
|
}
|
||||||
},
|
return i18nstringLocalize(props.position.item.name)
|
||||||
itemvar() {
|
})
|
||||||
if (this.position.variation) {
|
|
||||||
return `${i18nstring_localize(this.position.item.name)} – ${i18nstring_localize(this.position.variation.value)}`
|
const subevent = computed(() => formatSubevent(props.position.subevent))
|
||||||
}
|
|
||||||
return i18nstring_localize(this.position.item.name)
|
defineExpose({ el: rootEl })
|
||||||
},
|
|
||||||
subevent() {
|
|
||||||
if (!this.position.subevent) return ''
|
|
||||||
const name = i18nstring_localize(this.position.subevent.name)
|
|
||||||
const date = moment.utc(this.position.subevent.date_from).tz(this.$root.timezone).format(this.$root.datetime_format)
|
|
||||||
return `${name} · ${date}`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// secret
|
|
||||||
// status
|
|
||||||
// order code
|
|
||||||
// name
|
|
||||||
// seat
|
|
||||||
// require attention
|
|
||||||
</script>
|
</script>
|
||||||
|
<template lang="pug">
|
||||||
|
a.list-group-item.searchresult(ref="rootEl", href="#", @click.prevent="$emit('selected', position)")
|
||||||
|
.details
|
||||||
|
h4 {{ position.order }}-{{ position.positionid }} {{ position.attendee_name }}
|
||||||
|
span {{ itemvar }}
|
||||||
|
br
|
||||||
|
span(v-if="subevent") {{ subevent }}
|
||||||
|
br
|
||||||
|
.secret {{ position.secret }}
|
||||||
|
.status(:class="`status-${status}`")
|
||||||
|
span(v-if="position.require_attention")
|
||||||
|
span.fa.fa-warning
|
||||||
|
br
|
||||||
|
| {{ STRINGS[`status.${status}`] }}
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -1,54 +1,64 @@
|
|||||||
<template>
|
<script setup lang="ts">
|
||||||
<input class="form-control" :required="required">
|
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||||
</template>
|
import { timeFormat, datetimeLocale } from '../i18n'
|
||||||
<script>
|
|
||||||
export default {
|
const props = defineProps<{
|
||||||
props: ["required", "value"],
|
required?: boolean
|
||||||
mounted: function () {
|
modelValue?: string
|
||||||
var vm = this;
|
id?: string
|
||||||
var multiple = this.multiple;
|
}>()
|
||||||
$(this.$el)
|
|
||||||
.datetimepicker(this.opts())
|
const emit = defineEmits<{
|
||||||
.trigger("change")
|
'update:modelValue': [value: string]
|
||||||
.on("dp.change", function (e) {
|
}>()
|
||||||
vm.$emit("input", $(this).data('DateTimePicker').date().format("HH:mm:ss"));
|
|
||||||
});
|
const input = ref<HTMLInputElement>()
|
||||||
if (!vm.value) {
|
|
||||||
$(this.$el).data("DateTimePicker").viewDate(moment().hour(0).minute(0).second(0).millisecond(0));
|
const opts = {
|
||||||
} else {
|
format: timeFormat,
|
||||||
$(this.$el).data("DateTimePicker").date(moment(vm.value));
|
locale: datetimeLocale,
|
||||||
}
|
useCurrent: false,
|
||||||
},
|
showClear: props.required,
|
||||||
methods: {
|
icons: {
|
||||||
opts: function () {
|
time: 'fa fa-clock-o',
|
||||||
return {
|
date: 'fa fa-calendar',
|
||||||
format: $("body").attr("data-timeformat"),
|
up: 'fa fa-chevron-up',
|
||||||
locale: $("body").attr("data-datetimelocale"),
|
down: 'fa fa-chevron-down',
|
||||||
useCurrent: false,
|
previous: 'fa fa-chevron-left',
|
||||||
showClear: this.required,
|
next: 'fa fa-chevron-right',
|
||||||
icons: {
|
today: 'fa fa-screenshot',
|
||||||
time: 'fa fa-clock-o',
|
clear: 'fa fa-trash',
|
||||||
date: 'fa fa-calendar',
|
close: 'fa fa-remove',
|
||||||
up: 'fa fa-chevron-up',
|
},
|
||||||
down: 'fa fa-chevron-down',
|
|
||||||
previous: 'fa fa-chevron-left',
|
|
||||||
next: 'fa fa-chevron-right',
|
|
||||||
today: 'fa fa-screenshot',
|
|
||||||
clear: 'fa fa-trash',
|
|
||||||
close: 'fa fa-remove'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
value: function (val) {
|
|
||||||
$(this.$el).data('DateTimePicker').date(moment(val));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
destroyed: function () {
|
|
||||||
$(this.$el)
|
|
||||||
.off()
|
|
||||||
.datetimepicker("destroy");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
if (val) {
|
||||||
|
$(input.value!).data('DateTimePicker').date(moment(val))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
$(input.value!)
|
||||||
|
.datetimepicker(opts)
|
||||||
|
.trigger('change')
|
||||||
|
.on('dp.change', function (this: HTMLElement) {
|
||||||
|
emit('update:modelValue', $(this).data('DateTimePicker').date().format('HH:mm:ss'))
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!props.modelValue) {
|
||||||
|
$(input.value!).data('DateTimePicker').viewDate(moment().hour(0).minute(0).second(0).millisecond(0))
|
||||||
|
} else {
|
||||||
|
$(input.value!).data('DateTimePicker').date(moment(props.modelValue))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
$(input.value!)
|
||||||
|
.off()
|
||||||
|
.datetimepicker('destroy')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template lang="pug">
|
||||||
|
input.form-control(:id="id", ref="input", :required="required")
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
const body = document.body
|
||||||
|
|
||||||
|
export const timezone = body.dataset.timezone ?? 'UTC'
|
||||||
|
export const datetimeFormat = body.dataset.datetimeformat ?? 'L LT'
|
||||||
|
export const dateFormat = body.dataset.dateformat ?? 'L'
|
||||||
|
export const timeFormat = body.dataset.timeformat ?? 'LT'
|
||||||
|
export const datetimeLocale = body.dataset.datetimelocale ?? 'en'
|
||||||
|
export const pretixLocale = body.dataset.pretixlocale ?? 'en'
|
||||||
|
|
||||||
|
moment.locale(datetimeLocale)
|
||||||
|
|
||||||
|
export function gettext (msgid: string): string {
|
||||||
|
if (typeof django !== 'undefined' && typeof django.gettext !== 'undefined') {
|
||||||
|
return django.gettext(msgid)
|
||||||
|
}
|
||||||
|
return msgid
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ngettext (singular: string, plural: string, count: number): string {
|
||||||
|
if (typeof django !== 'undefined' && typeof django.ngettext !== 'undefined') {
|
||||||
|
return django.ngettext(singular, plural, count)
|
||||||
|
}
|
||||||
|
return plural
|
||||||
|
}
|
||||||
|
|
||||||
|
export type I18nString = string | Record<string, string> | null | undefined
|
||||||
|
|
||||||
|
export function i18nstringLocalize (obj: I18nString): string {
|
||||||
|
// external
|
||||||
|
return i18nstring_localize(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const STRINGS: Record<string, string> = {
|
||||||
|
'checkinlist.select': gettext('Select a check-in list'),
|
||||||
|
'checkinlist.none': gettext('No active check-in lists found.'),
|
||||||
|
'checkinlist.switch': gettext('Switch check-in list'),
|
||||||
|
'results.headline': gettext('Search results'),
|
||||||
|
'results.none': gettext('No tickets found'),
|
||||||
|
'check.headline': gettext('Result'),
|
||||||
|
'check.attention': gettext('This ticket requires special attention'),
|
||||||
|
'scantype.switch': gettext('Switch direction'),
|
||||||
|
'scantype.entry': gettext('Entry'),
|
||||||
|
'scantype.exit': gettext('Exit'),
|
||||||
|
'input.placeholder': gettext('Scan a ticket or search and press return…'),
|
||||||
|
'pagination.next': gettext('Load more'),
|
||||||
|
'status.p': gettext('Valid'),
|
||||||
|
'status.n': gettext('Unpaid'),
|
||||||
|
'status.c': gettext('Canceled'),
|
||||||
|
'status.e': gettext('Canceled'),
|
||||||
|
'status.pending_valid': gettext('Confirmed'),
|
||||||
|
'status.require_approval': gettext('Approval pending'),
|
||||||
|
'status.redeemed': gettext('Redeemed'),
|
||||||
|
'modal.cancel': gettext('Cancel'),
|
||||||
|
'modal.continue': gettext('Continue'),
|
||||||
|
'modal.unpaid.head': gettext('Ticket not paid'),
|
||||||
|
'modal.unpaid.text': gettext('This ticket is not yet paid. Do you want to continue anyways?'),
|
||||||
|
'modal.questions': gettext('Additional information required'),
|
||||||
|
'result.ok': gettext('Valid ticket'),
|
||||||
|
'result.exit': gettext('Exit recorded'),
|
||||||
|
'result.already_redeemed': gettext('Ticket already used'),
|
||||||
|
'result.questions': gettext('Information required'),
|
||||||
|
'result.invalid': gettext('Unknown ticket'),
|
||||||
|
'result.product': gettext('Ticket type not allowed here'),
|
||||||
|
'result.unpaid': gettext('Ticket not paid'),
|
||||||
|
'result.rules': gettext('Entry not allowed'),
|
||||||
|
'result.revoked': gettext('Ticket code revoked/changed'),
|
||||||
|
'result.blocked': gettext('Ticket blocked'),
|
||||||
|
'result.invalid_time': gettext('Ticket not valid at this time'),
|
||||||
|
'result.canceled': gettext('Order canceled'),
|
||||||
|
'result.ambiguous': gettext('Ticket code is ambiguous on list'),
|
||||||
|
'result.unapproved': gettext('Order not approved'),
|
||||||
|
'status.checkin': gettext('Checked-in Tickets'),
|
||||||
|
'status.position': gettext('Valid Tickets'),
|
||||||
|
'status.inside': gettext('Currently inside'),
|
||||||
|
yes: gettext('Yes'),
|
||||||
|
no: gettext('No'),
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubEvent {
|
||||||
|
name: Record<string, string>
|
||||||
|
date_from: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSubevent (subevent: SubEvent | null | undefined): string {
|
||||||
|
if (!subevent) return ''
|
||||||
|
const name = i18nstringLocalize(subevent.name)
|
||||||
|
const date = moment.utc(subevent.date_from).tz(timezone).format(datetimeFormat)
|
||||||
|
return `${name} · ${date}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Question {
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatAnswer (value: string, question: Question): string {
|
||||||
|
if (question.type === 'B' && value === 'True') {
|
||||||
|
return STRINGS['yes']
|
||||||
|
} else if (question.type === 'B' && value === 'False') {
|
||||||
|
return STRINGS['no']
|
||||||
|
} else if (question.type === 'W' && value) {
|
||||||
|
return moment(value).tz(timezone).format('L LT')
|
||||||
|
} else if (question.type === 'D' && value) {
|
||||||
|
return moment(value).format('L')
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
/*global gettext, Vue, App*/
|
|
||||||
function gettext(msgid) {
|
|
||||||
if (typeof django !== 'undefined' && typeof django.gettext !== 'undefined') {
|
|
||||||
return django.gettext(msgid);
|
|
||||||
}
|
|
||||||
return msgid;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ngettext(singular, plural, count) {
|
|
||||||
if (typeof django !== 'undefined' && typeof django.ngettext !== 'undefined') {
|
|
||||||
return django.ngettext(singular, plural, count);
|
|
||||||
}
|
|
||||||
return plural;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
moment.locale(document.body.attributes['data-datetimelocale'].value)
|
|
||||||
window.vapp = new Vue({
|
|
||||||
components: {
|
|
||||||
App: App.default
|
|
||||||
},
|
|
||||||
render: function (h) {
|
|
||||||
return h('App')
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
api: {
|
|
||||||
lists: document.querySelector('#app').attributes['data-api-lists'].value,
|
|
||||||
},
|
|
||||||
strings: {
|
|
||||||
'checkinlist.select': gettext('Select a check-in list'),
|
|
||||||
'checkinlist.none': gettext('No active check-in lists found.'),
|
|
||||||
'checkinlist.switch': gettext('Switch check-in list'),
|
|
||||||
'results.headline': gettext('Search results'),
|
|
||||||
'results.none': gettext('No tickets found'),
|
|
||||||
'check.headline': gettext('Result'),
|
|
||||||
'check.attention': gettext('This ticket requires special attention'),
|
|
||||||
'scantype.switch': gettext('Switch direction'),
|
|
||||||
'scantype.entry': gettext('Entry'),
|
|
||||||
'scantype.exit': gettext('Exit'),
|
|
||||||
'input.placeholder': gettext('Scan a ticket or search and press return…'),
|
|
||||||
'pagination.next': gettext('Load more'),
|
|
||||||
'status.p': gettext('Valid'),
|
|
||||||
'status.n': gettext('Unpaid'),
|
|
||||||
'status.c': gettext('Canceled'),
|
|
||||||
'status.e': gettext('Canceled'),
|
|
||||||
'status.pending_valid': gettext('Confirmed'),
|
|
||||||
'status.require_approval': gettext('Approval pending'),
|
|
||||||
'status.redeemed': gettext('Redeemed'),
|
|
||||||
'modal.cancel': gettext('Cancel'),
|
|
||||||
'modal.continue': gettext('Continue'),
|
|
||||||
'modal.unpaid.head': gettext('Ticket not paid'),
|
|
||||||
'modal.unpaid.text': gettext('This ticket is not yet paid. Do you want to continue anyways?'),
|
|
||||||
'modal.questions': gettext('Additional information required'),
|
|
||||||
'result.ok': gettext('Valid ticket'),
|
|
||||||
'result.exit': gettext('Exit recorded'),
|
|
||||||
'result.already_redeemed': gettext('Ticket already used'),
|
|
||||||
'result.questions': gettext('Information required'),
|
|
||||||
'result.invalid': gettext('Unknown ticket'),
|
|
||||||
'result.product': gettext('Ticket type not allowed here'),
|
|
||||||
'result.unpaid': gettext('Ticket not paid'),
|
|
||||||
'result.rules': gettext('Entry not allowed'),
|
|
||||||
'result.revoked': gettext('Ticket code revoked/changed'),
|
|
||||||
'result.blocked': gettext('Ticket blocked'),
|
|
||||||
'result.invalid_time': gettext('Ticket not valid at this time'),
|
|
||||||
'result.canceled': gettext('Order canceled'),
|
|
||||||
'result.ambiguous': gettext('Ticket code is ambiguous on list'),
|
|
||||||
'result.unapproved': gettext('Order not approved'),
|
|
||||||
'status.checkin': gettext('Checked-in Tickets'),
|
|
||||||
'status.position': gettext('Valid Tickets'),
|
|
||||||
'status.inside': gettext('Currently inside'),
|
|
||||||
'yes': gettext('Yes'),
|
|
||||||
'no': gettext('No'),
|
|
||||||
},
|
|
||||||
event_name: document.querySelector('#app').attributes['data-event-name'].value,
|
|
||||||
timezone: document.body.attributes['data-timezone'].value,
|
|
||||||
datetime_format: document.body.attributes['data-datetimeformat'].value,
|
|
||||||
},
|
|
||||||
el: '#app'
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
|
||||||
|
// import './scss/main.scss'
|
||||||
|
|
||||||
|
import App from './components/app.vue'
|
||||||
|
|
||||||
|
const mountEl = document.querySelector<HTMLElement>('#app')!
|
||||||
|
|
||||||
|
const app = createApp(App, mountEl.dataset)
|
||||||
|
app.mount('#app')
|
||||||
|
|
||||||
|
app.config.errorHandler = (error, _vm, info) => {
|
||||||
|
// vue fatals on errors by default, which is a weird choice
|
||||||
|
// https://github.com/vuejs/core/issues/3525
|
||||||
|
// https://github.com/vuejs/router/discussions/2435
|
||||||
|
console.error('[VUE]', info, error)
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
{% load statici18n %}
|
{% load statici18n %}
|
||||||
{% load eventurl %}
|
{% load eventurl %}
|
||||||
{% load escapejson %}
|
{% load escapejson %}
|
||||||
|
{% load vite %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -23,11 +24,7 @@
|
|||||||
<body data-datetimeformat="{{ js_datetime_format }}" data-timeformat="{{ js_time_format }}"
|
<body data-datetimeformat="{{ js_datetime_format }}" data-timeformat="{{ js_time_format }}"
|
||||||
data-dateformat="{{ js_date_format }}" data-datetimelocale="{{ js_locale }}"
|
data-dateformat="{{ js_date_format }}" data-datetimelocale="{{ js_locale }}"
|
||||||
data-pretixlocale="{{ request.LANGUAGE_CODE }}" data-timezone="{{ request.event.settings.timezone }}">
|
data-pretixlocale="{{ request.LANGUAGE_CODE }}" data-timezone="{{ request.event.settings.timezone }}">
|
||||||
<div
|
<div id="app" data-event-name="{{ request.event.name }}"></div>
|
||||||
data-api-lists="{% url "api-v1:checkinlist-list" event=request.event.slug organizer=request.organizer.slug %}"
|
|
||||||
data-api-questions="{% url "api-v1:question-list" event=request.event.slug organizer=request.organizer.slug %}"
|
|
||||||
data-event-name="{{ request.event.name }}"
|
|
||||||
id="app"></div>
|
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
<script type="text/javascript" src="{% static "pretixbase/js/i18nstring.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixbase/js/i18nstring.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "moment/moment-with-locales.js" %}"></script>
|
<script type="text/javascript" src="{% static "moment/moment-with-locales.js" %}"></script>
|
||||||
@@ -35,22 +32,17 @@
|
|||||||
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
|
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "datetimepicker/bootstrap-datetimepicker.js" %}"></script>
|
<script type="text/javascript" src="{% static "datetimepicker/bootstrap-datetimepicker.js" %}"></script>
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
{% if DEBUG %}
|
|
||||||
<script type="text/javascript" src="{% static "vuejs/vue.js" %}"></script>
|
|
||||||
{% else %}
|
|
||||||
<script type="text/javascript" src="{% static "vuejs/vue.min.js" %}"></script>
|
|
||||||
{% endif %}
|
|
||||||
{% compress js %}
|
|
||||||
<script type="text/vue" src="{% static 'pretixplugins/webcheckin/components/checkinlist-item.vue' %}"></script>
|
|
||||||
<script type="text/vue" src="{% static 'pretixplugins/webcheckin/components/checkinlist-select.vue' %}"></script>
|
|
||||||
<script type="text/vue" src="{% static 'pretixplugins/webcheckin/components/searchresult-item.vue' %}"></script>
|
|
||||||
<script type="text/vue" src="{% static 'pretixplugins/webcheckin/components/datetimefield.vue' %}"></script>
|
|
||||||
<script type="text/vue" src="{% static 'pretixplugins/webcheckin/components/datefield.vue' %}"></script>
|
|
||||||
<script type="text/vue" src="{% static 'pretixplugins/webcheckin/components/timefield.vue' %}"></script>
|
|
||||||
<script type="text/vue" src="{% static 'pretixplugins/webcheckin/components/app.vue' %}"></script>
|
|
||||||
<script type="text/javascript" src="{% static "pretixplugins/webcheckin/main.js" %}"></script>
|
|
||||||
{% endcompress %}
|
|
||||||
<script type="application/json" id="countries">{{ countries|escapejson_dumps }}</script>
|
<script type="application/json" id="countries">{{ countries|escapejson_dumps }}</script>
|
||||||
|
<script type="application/json" id="api-settings">
|
||||||
|
{
|
||||||
|
"urls": {
|
||||||
|
"lists": "{% url "api-v1:checkinlist-list" event=request.event.slug organizer=request.organizer.slug %}",
|
||||||
|
"questions": "{% url "api-v1:question-list" event=request.event.slug organizer=request.organizer.slug %}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% vite_hmr %}
|
||||||
|
{% vite_asset "src/pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.ts" %}
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user