mirror of
https://github.com/pretix/pretix.git
synced 2026-11-15 02:07:08 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9520c411db | ||
|
|
749211c206 | ||
|
|
f346acb0d2 | ||
|
|
0478559fd7 | ||
|
|
137059dd4e | ||
|
|
bd0b55bdd3 |
No files matched your search
@@ -1 +0,0 @@
|
||||
/*
|
||||
@@ -5,6 +5,7 @@ import ts from 'typescript-eslint'
|
||||
import stylistic from '@stylistic/eslint-plugin'
|
||||
import vue from 'eslint-plugin-vue'
|
||||
import vuePug from 'eslint-plugin-vue-pug'
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||
|
||||
const ignores = globalIgnores([
|
||||
'**/node_modules',
|
||||
@@ -161,4 +162,5 @@ export default defineConfig([
|
||||
},
|
||||
},
|
||||
},
|
||||
eslintPluginPrettierRecommended,
|
||||
])
|
||||
Generated
+127
@@ -21,9 +21,12 @@
|
||||
"@vue/eslint-config-typescript": "^14.7.0",
|
||||
"@vue/language-plugin-pug": "^3.2.5",
|
||||
"eslint": "^10.0.3",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.7",
|
||||
"eslint-plugin-vue": "^10.8.0",
|
||||
"eslint-plugin-vue-pug": "^1.0.0-alpha.5",
|
||||
"globals": "^17.4.0",
|
||||
"prettier": "3.10.1",
|
||||
"pug": "^3.0.3",
|
||||
"sass-embedded": "^1.98.0",
|
||||
"smol-toml": "^1.6.1",
|
||||
@@ -92,6 +95,17 @@
|
||||
"dev": true,
|
||||
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
||||
},
|
||||
"node_modules/@dual-bundle/import-meta-resolve": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.2.1.tgz",
|
||||
"integrity": "sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/JounQin"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
||||
@@ -718,6 +732,19 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgr/core": {
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz",
|
||||
"integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/pkgr"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm-eabi": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz",
|
||||
@@ -1966,6 +1993,53 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-prettier": {
|
||||
"version": "10.1.8",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
|
||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint-config-prettier"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier": {
|
||||
"version": "5.5.7",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.7.tgz",
|
||||
"integrity": "sha512-WjHyn3GrFd+ZR0rzsLiM4zJZ35PZMOE0SUIxzGmnWNG0126yNfJKJzObmmkYDwHJfGlkz3PSlzyG9ez/LJntwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prettier-linter-helpers": "^1.0.1",
|
||||
"synckit": "^0.12.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint-plugin-prettier"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/eslint": ">=8.0.0",
|
||||
"eslint": ">=8.0.0",
|
||||
"eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0",
|
||||
"prettier": ">=3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/eslint": {
|
||||
"optional": true
|
||||
},
|
||||
"eslint-config-prettier": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-vue": {
|
||||
"version": "10.8.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.8.0.tgz",
|
||||
@@ -2190,6 +2264,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-diff": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
|
||||
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
@@ -3361,6 +3442,35 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.10.1.tgz",
|
||||
"integrity": "sha512-70vp3ZUYoPlFbF9uiTdXHUUCGEIp7d4+6p+8DYbkm2jP7x/3i/paXVF6dopSI3zd/CEitrZN1rc4i85xWmmXlg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-linter-helpers": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz",
|
||||
"integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-diff": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/promise": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
|
||||
@@ -4300,6 +4410,23 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/synckit": {
|
||||
"version": "0.12.3",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.12.3.tgz",
|
||||
"integrity": "sha512-U+/AXT6LWsxnj2vz9HeUuPWBNgdWxjyIEjEPdbJARC9txmgPzo2VEdqnFo4hHaNyMPtycyVJrSaQYeQv/PoyyA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dual-bundle/import-meta-resolve": "^4.2.1",
|
||||
"@pkgr/core": "^0.3.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/synckit"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
|
||||
@@ -39,9 +39,12 @@
|
||||
"@vue/eslint-config-typescript": "^14.7.0",
|
||||
"@vue/language-plugin-pug": "^3.2.5",
|
||||
"eslint": "^10.0.3",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.7",
|
||||
"eslint-plugin-vue": "^10.8.0",
|
||||
"eslint-plugin-vue-pug": "^1.0.0-alpha.5",
|
||||
"globals": "^17.4.0",
|
||||
"prettier": "3.10.1",
|
||||
"pug": "^3.0.3",
|
||||
"sass-embedded": "^1.98.0",
|
||||
"smol-toml": "^1.6.1",
|
||||
|
||||
@@ -197,6 +197,8 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
||||
'linked_giftcard',
|
||||
'info',
|
||||
'notes',
|
||||
# When adding fields, check if they should be hidden in ReusableMediaViewSet
|
||||
# for cross-organizer lookups
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -117,6 +117,7 @@ class TransmissionInfoSerializer(serializers.Serializer):
|
||||
super().__init__(*args, **kwargs)
|
||||
for k, v in transmission_type.invoice_address_form_fields.items():
|
||||
self.fields[k] = form_field_to_serializer_field(v)
|
||||
self.fields[k].required = False # invoice_address_form_fields_required is checked by parent serializer
|
||||
|
||||
|
||||
class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
||||
|
||||
@@ -183,9 +183,18 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
|
||||
type=s.validated_data["type"],
|
||||
identifier=s.validated_data["identifier"],
|
||||
)
|
||||
m.customer = None # not relevant for cross-organizer
|
||||
s = self.get_serializer(m)
|
||||
return Response({"result": s.data})
|
||||
data = s.data
|
||||
# Remove fields that are not relevant for cross-organizer usage
|
||||
data["customer"] = None
|
||||
data["linked_orderposition"] = None
|
||||
data["linked_orderpositions"] = []
|
||||
data["info"] = {}
|
||||
data["notes"] = None
|
||||
data["claim_token"] = None
|
||||
data["label"] = None
|
||||
|
||||
return Response({"result": data})
|
||||
except ReusableMedium.DoesNotExist:
|
||||
mt = MEDIA_TYPES.get(s.validated_data["type"])
|
||||
if mt:
|
||||
|
||||
@@ -27,6 +27,7 @@ from decimal import Decimal
|
||||
|
||||
from django import forms
|
||||
from django.db.models import F, Sum
|
||||
from django.db.models.aggregates import Min
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape
|
||||
@@ -41,7 +42,8 @@ from reportlab.platypus import (
|
||||
|
||||
from pretix.base.exporter import BaseExporter
|
||||
from pretix.base.models import (
|
||||
GiftCardTransaction, OrderFee, OrderPayment, OrderRefund, Transaction,
|
||||
GiftCardTransaction, Order, OrderFee, OrderPayment, OrderRefund,
|
||||
Transaction,
|
||||
)
|
||||
from pretix.base.templatetags.money import money_filter, tax_rate_format
|
||||
from pretix.base.timeframes import (
|
||||
@@ -116,25 +118,34 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
dt_start, df_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(
|
||||
now(), form_data["date_range"], self.timezone
|
||||
)
|
||||
if dt_start:
|
||||
filters.append(
|
||||
_("Begin")
|
||||
+ ": "
|
||||
+ date_format(
|
||||
else:
|
||||
dt_start = Order.objects.filter(
|
||||
event__in=self.events,
|
||||
).aggregate(min_datetime=Min('datetime'))['min_datetime']
|
||||
df_end = now()
|
||||
|
||||
if dt_start:
|
||||
filters.append(
|
||||
"{label}: {value}{hint}".format(
|
||||
label=_("Begin"),
|
||||
value=date_format(
|
||||
dt_start.astimezone(self.timezone), "SHORT_DATETIME_FORMAT"
|
||||
)
|
||||
),
|
||||
hint="" if form_data["date_range"] else " ({})".format(_("first order received"))
|
||||
)
|
||||
if df_end:
|
||||
filters.append(
|
||||
_("End")
|
||||
+ ": "
|
||||
+ date_format(
|
||||
(df_end - datetime.timedelta.resolution).astimezone(
|
||||
self.timezone
|
||||
),
|
||||
"SHORT_DATETIME_FORMAT",
|
||||
)
|
||||
)
|
||||
|
||||
if df_end:
|
||||
filters.append(
|
||||
_("End")
|
||||
+ ": "
|
||||
+ date_format(
|
||||
(df_end - datetime.timedelta.resolution).astimezone(
|
||||
self.timezone
|
||||
),
|
||||
"SHORT_DATETIME_FORMAT",
|
||||
)
|
||||
)
|
||||
|
||||
if not form_data["no_testmode"]:
|
||||
filters.append(_("Report includes test orders which may be deleted later!"))
|
||||
|
||||
@@ -1,271 +1,298 @@
|
||||
import type { I18nString, SubEvent } from './i18n'
|
||||
import type { I18nString, SubEvent } from "./i18n";
|
||||
|
||||
const settingsEl = document.getElementById('api-settings')
|
||||
const { urls } = JSON.parse(settingsEl.textContent || '{}') as { urls: {
|
||||
lists: string
|
||||
questions: string
|
||||
} }
|
||||
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[]
|
||||
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
|
||||
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
|
||||
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
|
||||
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>
|
||||
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[]
|
||||
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[]
|
||||
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[]
|
||||
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
|
||||
id: number;
|
||||
identifier: string;
|
||||
position: number;
|
||||
answer: I18nString;
|
||||
}
|
||||
|
||||
export interface Addon {
|
||||
item: { name: I18nString; internal_name?: string }
|
||||
variation: { value: I18nString } | null
|
||||
item: { name: I18nString; internal_name?: string };
|
||||
variation: { value: I18nString } | null;
|
||||
}
|
||||
|
||||
export interface CheckinStatusVariation {
|
||||
id: number
|
||||
value: string
|
||||
checkin_count: number
|
||||
position_count: number
|
||||
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[]
|
||||
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[]
|
||||
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
|
||||
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
|
||||
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
|
||||
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 ?? ''
|
||||
const CSRF_TOKEN =
|
||||
document.querySelector<HTMLInputElement>("input[name=csrfmiddlewaretoken]")
|
||||
?.value ?? "";
|
||||
|
||||
function handleAuthError (response: Response): void {
|
||||
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
|
||||
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)
|
||||
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)
|
||||
throw new Error("HTTP status " + response.status);
|
||||
}
|
||||
return response.json()
|
||||
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 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 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 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 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 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 (
|
||||
async redeemPosition(
|
||||
listId: number,
|
||||
positionId: string,
|
||||
data: RedeemRequest,
|
||||
untrusted: boolean = false
|
||||
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'
|
||||
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',
|
||||
method: "POST",
|
||||
headers: {
|
||||
'X-CSRFToken': CSRF_TOKEN,
|
||||
'Content-Type': 'application/json',
|
||||
"X-CSRFToken": CSRF_TOKEN,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
});
|
||||
|
||||
handleAuthError(response)
|
||||
handleAuthError(response);
|
||||
|
||||
if (response.status === 404) {
|
||||
return { status: 'error', reason: 'invalid' }
|
||||
return { status: "error", reason: "invalid" };
|
||||
}
|
||||
|
||||
if (!response.ok && response.status !== 400) {
|
||||
throw new Error('HTTP status ' + response.status)
|
||||
throw new Error("HTTP status " + response.status);
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
};
|
||||
+247
-209
@@ -1,292 +1,316 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick, onMounted, onUnmounted } from 'vue'
|
||||
import { api } from '../api'
|
||||
import type { CheckinList, Position, RedeemResponse, CheckinStatus, AnswerQuestion } from '../api'
|
||||
import { STRINGS, i18nstringLocalize, formatSubevent, formatAnswer } from '../i18n'
|
||||
import { ref, computed, nextTick, onMounted, onUnmounted } from "vue";
|
||||
import { api } from "../api";
|
||||
import type {
|
||||
CheckinList,
|
||||
Position,
|
||||
RedeemResponse,
|
||||
CheckinStatus,
|
||||
AnswerQuestion,
|
||||
} from "../api";
|
||||
import {
|
||||
STRINGS,
|
||||
i18nstringLocalize,
|
||||
formatSubevent,
|
||||
formatAnswer,
|
||||
} from "../i18n";
|
||||
|
||||
import CheckinlistSelect from './checkinlist-select.vue'
|
||||
import SearchresultItem from './searchresult-item.vue'
|
||||
import Datefield from './datefield.vue'
|
||||
import Timefield from './timefield.vue'
|
||||
import Datetimefield from './datetimefield.vue'
|
||||
import CheckinlistSelect from "./checkinlist-select.vue";
|
||||
import SearchresultItem from "./searchresult-item.vue";
|
||||
import Datefield from "./datefield.vue";
|
||||
import Timefield from "./timefield.vue";
|
||||
import Datetimefield from "./datetimefield.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
eventName?: string
|
||||
}>()
|
||||
eventName?: string;
|
||||
}>();
|
||||
|
||||
const COUNTRIES = JSON.parse(document.querySelector('#countries')?.textContent ?? '[]')
|
||||
const COUNTRIES = JSON.parse(
|
||||
document.querySelector("#countries")?.textContent ?? "[]",
|
||||
);
|
||||
|
||||
let clearTimeoutId: ReturnType<typeof setTimeout> | null = null
|
||||
const answers = ref<Record<string, string>>({})
|
||||
let clearTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
const answers = ref<Record<string, string>>({});
|
||||
|
||||
// Checkin list selection
|
||||
const checkinlist = ref<CheckinList | null>(null)
|
||||
const subevent = computed(() => formatSubevent(checkinlist.value?.subevent))
|
||||
const checkinlist = ref<CheckinList | null>(null);
|
||||
const subevent = computed(() => formatSubevent(checkinlist.value?.subevent));
|
||||
|
||||
function selectList (list: CheckinList) {
|
||||
checkinlist.value = list
|
||||
location.hash = '#' + list.id
|
||||
refocus()
|
||||
fetchStatus()
|
||||
function selectList(list: CheckinList) {
|
||||
checkinlist.value = list;
|
||||
location.hash = "#" + list.id;
|
||||
refocus();
|
||||
fetchStatus();
|
||||
}
|
||||
|
||||
function switchList () {
|
||||
location.hash = ''
|
||||
checkinlist.value = null
|
||||
function switchList() {
|
||||
location.hash = "";
|
||||
checkinlist.value = null;
|
||||
}
|
||||
|
||||
// Entry/Exit type
|
||||
const type = ref<'entry' | 'exit'>('entry')
|
||||
const type = ref<"entry" | "exit">("entry");
|
||||
|
||||
function switchType () {
|
||||
type.value = type.value === 'exit' ? 'entry' : 'exit'
|
||||
refocus()
|
||||
function switchType() {
|
||||
type.value = type.value === "exit" ? "entry" : "exit";
|
||||
refocus();
|
||||
}
|
||||
|
||||
// Input/query
|
||||
const inputEl = ref<HTMLInputElement>()
|
||||
const query = ref('')
|
||||
const inputEl = ref<HTMLInputElement>();
|
||||
const query = ref("");
|
||||
|
||||
function refocus () {
|
||||
function refocus() {
|
||||
nextTick(() => {
|
||||
inputEl.value?.focus()
|
||||
})
|
||||
inputEl.value?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function inputKeyup (e: KeyboardEvent) {
|
||||
if (e.key === 'Enter') {
|
||||
startSearch(true)
|
||||
} else if (query.value === '') {
|
||||
clear()
|
||||
function inputKeyup(e: KeyboardEvent) {
|
||||
if (e.key === "Enter") {
|
||||
startSearch(true);
|
||||
} else if (query.value === "") {
|
||||
clear();
|
||||
}
|
||||
}
|
||||
|
||||
function globalKeydown (e: KeyboardEvent | Event) {
|
||||
function globalKeydown(e: KeyboardEvent | Event) {
|
||||
if (!(e instanceof KeyboardEvent)) {
|
||||
refocus()
|
||||
return
|
||||
refocus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.activeElement?.classList.contains('searchresult')) {
|
||||
if (e.key === 'ArrowDown') {
|
||||
(document.activeElement.nextElementSibling as HTMLElement)?.focus()
|
||||
e.preventDefault()
|
||||
return
|
||||
if (document.activeElement?.classList.contains("searchresult")) {
|
||||
if (e.key === "ArrowDown") {
|
||||
(document.activeElement.nextElementSibling as HTMLElement)?.focus();
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (e.key === 'ArrowUp') {
|
||||
(document.activeElement.previousElementSibling as HTMLElement)?.focus()
|
||||
e.preventDefault()
|
||||
return
|
||||
if (e.key === "ArrowUp") {
|
||||
(document.activeElement.previousElementSibling as HTMLElement)?.focus();
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const nodeName = document.activeElement?.nodeName?.toLowerCase()
|
||||
if (nodeName !== 'input' && nodeName !== 'textarea') {
|
||||
const nodeName = document.activeElement?.nodeName?.toLowerCase();
|
||||
if (nodeName !== "input" && nodeName !== "textarea") {
|
||||
if (e.key?.match(/^[a-z0-9A-Z+/=<>#]$/)) {
|
||||
query.value = ''
|
||||
refocus()
|
||||
query.value = "";
|
||||
refocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Status
|
||||
const status = ref<CheckinStatus | null>(null)
|
||||
const statusLoading = ref(0)
|
||||
let statusInterval: ReturnType<typeof setInterval> | null = null
|
||||
const status = ref<CheckinStatus | null>(null);
|
||||
const statusLoading = ref(0);
|
||||
let statusInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
async function fetchStatus () {
|
||||
if (!checkinlist.value) return
|
||||
async function fetchStatus() {
|
||||
if (!checkinlist.value) return;
|
||||
|
||||
statusLoading.value++
|
||||
statusLoading.value++;
|
||||
try {
|
||||
status.value = await api.fetchStatus(checkinlist.value.id)
|
||||
status.value = await api.fetchStatus(checkinlist.value.id);
|
||||
} finally {
|
||||
statusLoading.value--
|
||||
statusLoading.value--;
|
||||
}
|
||||
}
|
||||
|
||||
// Check/redeem
|
||||
const checkLoading = ref(false)
|
||||
const checkError = ref<string | null>(null)
|
||||
const checkResult = ref<RedeemResponse | null>(null)
|
||||
const checkLoading = ref(false);
|
||||
const checkError = ref<string | null>(null);
|
||||
const checkResult = ref<RedeemResponse | null>(null);
|
||||
|
||||
const checkResultAddons = computed(() => {
|
||||
if (!checkResult.value?.position?.addons) return ''
|
||||
return checkResult.value.position.addons.map((addon) => {
|
||||
if (addon.variation) {
|
||||
return `+ ${addon.item.internal_name || i18nstringLocalize(addon.item.name)} – ${i18nstringLocalize(addon.variation.value)}`
|
||||
}
|
||||
return '+ ' + (addon.item.internal_name || i18nstringLocalize(addon.item.name))
|
||||
}).join('\n')
|
||||
})
|
||||
if (!checkResult.value?.position?.addons) return "";
|
||||
return checkResult.value.position.addons
|
||||
.map((addon) => {
|
||||
if (addon.variation) {
|
||||
return `+ ${addon.item.internal_name || i18nstringLocalize(addon.item.name)} – ${i18nstringLocalize(addon.variation.value)}`;
|
||||
}
|
||||
return (
|
||||
"+ " + (addon.item.internal_name || i18nstringLocalize(addon.item.name))
|
||||
);
|
||||
})
|
||||
.join("\n");
|
||||
});
|
||||
|
||||
const checkResultSubevent = computed(() => formatSubevent(checkResult.value?.position?.subevent))
|
||||
const checkResultSubevent = computed(() =>
|
||||
formatSubevent(checkResult.value?.position?.subevent),
|
||||
);
|
||||
|
||||
const checkResultItemvar = computed(() => {
|
||||
if (!checkResult.value?.position) return ''
|
||||
if (!checkResult.value?.position) return "";
|
||||
if (checkResult.value.position.variation) {
|
||||
return `${i18nstringLocalize(checkResult.value.position.item.name)} – ${i18nstringLocalize(checkResult.value.position.variation.value)}`
|
||||
return `${i18nstringLocalize(checkResult.value.position.item.name)} – ${i18nstringLocalize(checkResult.value.position.variation.value)}`;
|
||||
}
|
||||
return i18nstringLocalize(checkResult.value.position.item.name)
|
||||
})
|
||||
return i18nstringLocalize(checkResult.value.position.item.name);
|
||||
});
|
||||
|
||||
const checkResultText = computed(() => {
|
||||
if (!checkResult.value) return ''
|
||||
if (checkResult.value.status === 'ok') {
|
||||
return type.value === 'exit' ? STRINGS['result.exit'] : STRINGS['result.ok']
|
||||
} else if (checkResult.value.status === 'incomplete') {
|
||||
return STRINGS['result.questions']
|
||||
} else if (checkResult.value.reason && STRINGS['result.' + checkResult.value.reason]) {
|
||||
return STRINGS['result.' + checkResult.value.reason]
|
||||
if (!checkResult.value) return "";
|
||||
if (checkResult.value.status === "ok") {
|
||||
return type.value === "exit"
|
||||
? STRINGS["result.exit"]
|
||||
: STRINGS["result.ok"];
|
||||
} else if (checkResult.value.status === "incomplete") {
|
||||
return STRINGS["result.questions"];
|
||||
} else if (
|
||||
checkResult.value.reason &&
|
||||
STRINGS["result." + checkResult.value.reason]
|
||||
) {
|
||||
return STRINGS["result." + checkResult.value.reason];
|
||||
}
|
||||
return checkResult.value.reason ?? ''
|
||||
})
|
||||
return checkResult.value.reason ?? "";
|
||||
});
|
||||
|
||||
const checkResultColor = computed(() => {
|
||||
if (!checkResult.value) return ''
|
||||
if (checkResult.value.status === 'ok') return 'green'
|
||||
if (checkResult.value.status === 'incomplete') return 'purple'
|
||||
if (checkResult.value.reason === 'already_redeemed') return 'orange'
|
||||
return 'red'
|
||||
})
|
||||
if (!checkResult.value) return "";
|
||||
if (checkResult.value.status === "ok") return "green";
|
||||
if (checkResult.value.status === "incomplete") return "purple";
|
||||
if (checkResult.value.reason === "already_redeemed") return "orange";
|
||||
return "red";
|
||||
});
|
||||
|
||||
// Modals
|
||||
const showUnpaidModal = ref(false)
|
||||
const showQuestionsModal = ref(false)
|
||||
const questionsModalEl = ref<HTMLFormElement>()
|
||||
const showUnpaidModal = ref(false);
|
||||
const showQuestionsModal = ref(false);
|
||||
const questionsModalEl = ref<HTMLFormElement>();
|
||||
|
||||
function answerSetM (qid: string, opid: string, checked: boolean) {
|
||||
let arr = answers.value[qid] ? answers.value[qid].split(',') : []
|
||||
function answerSetM(qid: string, opid: string, checked: boolean) {
|
||||
let arr = answers.value[qid] ? answers.value[qid].split(",") : [];
|
||||
if (checked && !arr.includes(opid)) {
|
||||
arr.push(opid)
|
||||
arr.push(opid);
|
||||
} else if (!checked) {
|
||||
arr = arr.filter(o => opid !== o)
|
||||
arr = arr.filter((o) => opid !== o);
|
||||
}
|
||||
answers.value[qid] = arr.join(',')
|
||||
answers.value[qid] = arr.join(",");
|
||||
}
|
||||
|
||||
// Search
|
||||
const searchLoading = ref(false)
|
||||
const searchResults = ref<Position[] | null>(null)
|
||||
const searchNextUrl = ref<string | null>(null)
|
||||
const searchError = ref<unknown>(null)
|
||||
const resultEls = ref<InstanceType<typeof SearchresultItem>[]>([])
|
||||
const searchLoading = ref(false);
|
||||
const searchResults = ref<Position[] | null>(null);
|
||||
const searchNextUrl = ref<string | null>(null);
|
||||
const searchError = ref<unknown>(null);
|
||||
const resultEls = ref<InstanceType<typeof SearchresultItem>[]>([]);
|
||||
|
||||
function selectResult (position: Position) {
|
||||
check(position.secret, false, false, false, false)
|
||||
function selectResult(position: Position) {
|
||||
check(position.secret, false, false, false, false);
|
||||
}
|
||||
|
||||
async function startSearch (fallbackToScan: boolean) {
|
||||
async function startSearch(fallbackToScan: boolean) {
|
||||
if (query.value.length >= 32 && fallbackToScan) {
|
||||
check(query.value, false, false, true, true)
|
||||
return
|
||||
check(query.value, false, false, true, true);
|
||||
return;
|
||||
}
|
||||
|
||||
checkResult.value = null
|
||||
searchLoading.value = true
|
||||
searchError.value = null
|
||||
searchResults.value = []
|
||||
answers.value = {}
|
||||
checkResult.value = null;
|
||||
searchLoading.value = true;
|
||||
searchError.value = null;
|
||||
searchResults.value = [];
|
||||
answers.value = {};
|
||||
|
||||
if (clearTimeoutId) {
|
||||
clearTimeout(clearTimeoutId)
|
||||
clearTimeoutId = null
|
||||
clearTimeout(clearTimeoutId);
|
||||
clearTimeoutId = null;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await api.searchPositions(checkinlist.value!.id, query.value)
|
||||
searchLoading.value = false
|
||||
const data = await api.searchPositions(checkinlist.value!.id, query.value);
|
||||
searchLoading.value = false;
|
||||
|
||||
if (data.results) {
|
||||
searchResults.value = data.results
|
||||
searchNextUrl.value = data.next
|
||||
searchResults.value = data.results;
|
||||
searchNextUrl.value = data.next;
|
||||
|
||||
if (data.results.length) {
|
||||
if (data.results[0].secret === query.value) {
|
||||
nextTick(() => {
|
||||
inputEl.value?.blur()
|
||||
resultEls.value[0]?.el?.click()
|
||||
})
|
||||
inputEl.value?.blur();
|
||||
resultEls.value[0]?.el?.click();
|
||||
});
|
||||
} else {
|
||||
nextTick(() => {
|
||||
resultEls.value[0]?.el?.focus()
|
||||
})
|
||||
resultEls.value[0]?.el?.focus();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
nextTick(() => {
|
||||
inputEl.value?.blur()
|
||||
})
|
||||
inputEl.value?.blur();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
searchError.value = data
|
||||
searchError.value = data;
|
||||
}
|
||||
|
||||
clearTimeoutId = setTimeout(clear, 30 * 1000)
|
||||
clearTimeoutId = setTimeout(clear, 30 * 1000);
|
||||
} catch (e) {
|
||||
searchLoading.value = false
|
||||
searchResults.value = []
|
||||
searchError.value = e
|
||||
clearTimeoutId = setTimeout(clear, 30 * 1000)
|
||||
searchLoading.value = false;
|
||||
searchResults.value = [];
|
||||
searchError.value = e;
|
||||
clearTimeoutId = setTimeout(clear, 30 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
async function searchNext () {
|
||||
if (!searchNextUrl.value) return
|
||||
async function searchNext() {
|
||||
if (!searchNextUrl.value) return;
|
||||
|
||||
searchLoading.value = true
|
||||
searchError.value = null
|
||||
searchLoading.value = true;
|
||||
searchError.value = null;
|
||||
|
||||
if (clearTimeoutId) {
|
||||
clearTimeout(clearTimeoutId)
|
||||
clearTimeoutId = null
|
||||
clearTimeout(clearTimeoutId);
|
||||
clearTimeoutId = null;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await api.fetchNextPage<Position>(searchNextUrl.value)
|
||||
searchLoading.value = false
|
||||
const data = await api.fetchNextPage<Position>(searchNextUrl.value);
|
||||
searchLoading.value = false;
|
||||
|
||||
if (data.results) {
|
||||
searchResults.value = [...(searchResults.value ?? []), ...data.results]
|
||||
searchNextUrl.value = data.next
|
||||
searchResults.value = [...(searchResults.value ?? []), ...data.results];
|
||||
searchNextUrl.value = data.next;
|
||||
} else {
|
||||
searchError.value = data
|
||||
searchError.value = data;
|
||||
}
|
||||
|
||||
clearTimeoutId = setTimeout(clear, 30 * 1000)
|
||||
clearTimeoutId = setTimeout(clear, 30 * 1000);
|
||||
} catch (e) {
|
||||
searchLoading.value = false
|
||||
searchError.value = e
|
||||
clearTimeoutId = setTimeout(clear, 30 * 1000)
|
||||
searchLoading.value = false;
|
||||
searchError.value = e;
|
||||
clearTimeoutId = setTimeout(clear, 30 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
async function check (
|
||||
async function check(
|
||||
id: string,
|
||||
ignoreUnpaid: boolean,
|
||||
keepAnswers: boolean,
|
||||
fallbackToSearch: boolean,
|
||||
untrusted: boolean
|
||||
untrusted: boolean,
|
||||
) {
|
||||
if (!keepAnswers) {
|
||||
answers.value = {}
|
||||
answers.value = {};
|
||||
} else if (showQuestionsModal.value) {
|
||||
if (!questionsModalEl.value?.reportValidity()) return
|
||||
if (!questionsModalEl.value?.reportValidity()) return;
|
||||
}
|
||||
|
||||
showUnpaidModal.value = false
|
||||
showQuestionsModal.value = false
|
||||
checkLoading.value = true
|
||||
checkError.value = null
|
||||
checkResult.value = {} as RedeemResponse
|
||||
showUnpaidModal.value = false;
|
||||
showQuestionsModal.value = false;
|
||||
checkLoading.value = true;
|
||||
checkError.value = null;
|
||||
checkResult.value = {} as RedeemResponse;
|
||||
|
||||
if (clearTimeoutId) {
|
||||
clearTimeout(clearTimeoutId)
|
||||
clearTimeoutId = null
|
||||
clearTimeout(clearTimeoutId);
|
||||
clearTimeoutId = null;
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
inputEl.value?.blur()
|
||||
})
|
||||
inputEl.value?.blur();
|
||||
});
|
||||
|
||||
try {
|
||||
const data = await api.redeemPosition(
|
||||
@@ -299,73 +323,87 @@ async function check (
|
||||
type: type.value,
|
||||
answers: answers.value,
|
||||
},
|
||||
untrusted
|
||||
)
|
||||
untrusted,
|
||||
);
|
||||
|
||||
checkLoading.value = false
|
||||
checkResult.value = data
|
||||
checkLoading.value = false;
|
||||
checkResult.value = data;
|
||||
|
||||
if (checkinlist.value?.include_pending && data.status === 'error' && data.reason === 'unpaid') {
|
||||
showUnpaidModal.value = true
|
||||
if (
|
||||
checkinlist.value?.include_pending &&
|
||||
data.status === "error" &&
|
||||
data.reason === "unpaid"
|
||||
) {
|
||||
showUnpaidModal.value = true;
|
||||
nextTick(() => {
|
||||
document.querySelector<HTMLButtonElement>('.modal-unpaid .btn-primary')?.focus()
|
||||
})
|
||||
} else if (data.status === 'incomplete' && data.questions) {
|
||||
showQuestionsModal.value = true
|
||||
document
|
||||
.querySelector<HTMLButtonElement>(".modal-unpaid .btn-primary")
|
||||
?.focus();
|
||||
});
|
||||
} else if (data.status === "incomplete" && data.questions) {
|
||||
showQuestionsModal.value = true;
|
||||
for (const q of data.questions) {
|
||||
if (!answers.value[q.id.toString()]) {
|
||||
answers.value[q.id.toString()] = ''
|
||||
answers.value[q.id.toString()] = "";
|
||||
}
|
||||
;(q as AnswerQuestion).question = i18nstringLocalize(q.question)
|
||||
(q as AnswerQuestion).question = i18nstringLocalize(q.question);
|
||||
for (const o of q.options) {
|
||||
;(o as { answer: string }).answer = i18nstringLocalize(o.answer)
|
||||
(o as { answer: string }).answer = i18nstringLocalize(o.answer);
|
||||
}
|
||||
}
|
||||
nextTick(() => {
|
||||
document.querySelector<HTMLElement>('.modal-questions input, .modal-questions select, .modal-questions textarea')?.focus()
|
||||
})
|
||||
} else if (data.status === 'error' && data.reason === 'invalid' && fallbackToSearch) {
|
||||
startSearch(false)
|
||||
document
|
||||
.querySelector<HTMLElement>(
|
||||
".modal-questions input, .modal-questions select, .modal-questions textarea",
|
||||
)
|
||||
?.focus();
|
||||
});
|
||||
} else if (
|
||||
data.status === "error" &&
|
||||
data.reason === "invalid" &&
|
||||
fallbackToSearch
|
||||
) {
|
||||
startSearch(false);
|
||||
} else {
|
||||
clearTimeoutId = setTimeout(clear, 30 * 1000)
|
||||
fetchStatus()
|
||||
clearTimeoutId = setTimeout(clear, 30 * 1000);
|
||||
fetchStatus();
|
||||
}
|
||||
} catch (e) {
|
||||
checkLoading.value = false
|
||||
checkResult.value = {} as RedeemResponse
|
||||
checkError.value = String(e)
|
||||
clearTimeoutId = setTimeout(clear, 30 * 1000)
|
||||
checkLoading.value = false;
|
||||
checkResult.value = {} as RedeemResponse;
|
||||
checkError.value = String(e);
|
||||
clearTimeoutId = setTimeout(clear, 30 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function clear () {
|
||||
query.value = ''
|
||||
searchLoading.value = false
|
||||
searchResults.value = null
|
||||
searchNextUrl.value = null
|
||||
searchError.value = null
|
||||
checkLoading.value = false
|
||||
checkError.value = null
|
||||
checkResult.value = null
|
||||
showUnpaidModal.value = false
|
||||
showQuestionsModal.value = false
|
||||
answers.value = {}
|
||||
function clear() {
|
||||
query.value = "";
|
||||
searchLoading.value = false;
|
||||
searchResults.value = null;
|
||||
searchNextUrl.value = null;
|
||||
searchError.value = null;
|
||||
checkLoading.value = false;
|
||||
checkError.value = null;
|
||||
checkResult.value = null;
|
||||
showUnpaidModal.value = false;
|
||||
showQuestionsModal.value = false;
|
||||
answers.value = {};
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('focus', globalKeydown)
|
||||
document.addEventListener('visibilitychange', globalKeydown)
|
||||
document.addEventListener('keydown', globalKeydown)
|
||||
statusInterval = setInterval(fetchStatus, 120 * 1000)
|
||||
})
|
||||
window.addEventListener("focus", globalKeydown);
|
||||
document.addEventListener("visibilitychange", globalKeydown);
|
||||
document.addEventListener("keydown", globalKeydown);
|
||||
statusInterval = setInterval(fetchStatus, 120 * 1000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('focus', globalKeydown)
|
||||
document.removeEventListener('visibilitychange', globalKeydown)
|
||||
document.removeEventListener('keydown', globalKeydown)
|
||||
if (statusInterval) clearInterval(statusInterval)
|
||||
if (clearTimeoutId) clearTimeout(clearTimeoutId)
|
||||
})
|
||||
window.removeEventListener("focus", globalKeydown);
|
||||
document.removeEventListener("visibilitychange", globalKeydown);
|
||||
document.removeEventListener("keydown", globalKeydown);
|
||||
if (statusInterval) clearInterval(statusInterval);
|
||||
if (clearTimeoutId) clearTimeout(clearTimeoutId);
|
||||
});
|
||||
</script>
|
||||
<template lang="pug">
|
||||
.container
|
||||
|
||||
+8
-8
@@ -1,17 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { CheckinList } from '../api'
|
||||
import { formatSubevent } from '../i18n'
|
||||
import { computed } from "vue";
|
||||
import type { CheckinList } from "../api";
|
||||
import { formatSubevent } from "../i18n";
|
||||
|
||||
const props = defineProps<{
|
||||
list: CheckinList
|
||||
}>()
|
||||
list: CheckinList;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
selected: [list: CheckinList]
|
||||
}>()
|
||||
selected: [list: CheckinList];
|
||||
}>();
|
||||
|
||||
const subevent = computed(() => formatSubevent(props.list.subevent))
|
||||
const subevent = computed(() => formatSubevent(props.list.subevent));
|
||||
</script>
|
||||
<template lang="pug">
|
||||
a.list-group-item(href="#", @click.prevent="$emit('selected', list)")
|
||||
|
||||
+45
-45
@@ -1,86 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { api } from '../api'
|
||||
import type { CheckinList } from '../api'
|
||||
import { STRINGS } from '../i18n'
|
||||
import CheckinlistItem from './checkinlist-item.vue'
|
||||
import { ref, onMounted } from "vue";
|
||||
import { api } from "../api";
|
||||
import type { CheckinList } from "../api";
|
||||
import { STRINGS } from "../i18n";
|
||||
import CheckinlistItem from "./checkinlist-item.vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
selected: [list: CheckinList]
|
||||
}>()
|
||||
selected: [list: CheckinList];
|
||||
}>();
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref<unknown>(null)
|
||||
const lists = ref<CheckinList[] | null>(null)
|
||||
const nextUrl = ref<string | null>(null)
|
||||
const loading = ref(false);
|
||||
const error = ref<unknown>(null);
|
||||
const lists = ref<CheckinList[] | null>(null);
|
||||
const nextUrl = ref<string | null>(null);
|
||||
|
||||
async function load () {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
if (location.hash) {
|
||||
const listId = location.hash.substring(1)
|
||||
const listId = location.hash.substring(1);
|
||||
try {
|
||||
const data = await api.fetchCheckinList(listId)
|
||||
loading.value = false
|
||||
const data = await api.fetchCheckinList(listId);
|
||||
loading.value = false;
|
||||
if (data.id) {
|
||||
emit('selected', data)
|
||||
emit("selected", data);
|
||||
} else {
|
||||
location.hash = ''
|
||||
load()
|
||||
location.hash = "";
|
||||
load();
|
||||
}
|
||||
} catch {
|
||||
location.hash = ''
|
||||
load()
|
||||
location.hash = "";
|
||||
load();
|
||||
}
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await api.fetchCheckinLists()
|
||||
loading.value = false
|
||||
const data = await api.fetchCheckinLists();
|
||||
loading.value = false;
|
||||
|
||||
if (data.results) {
|
||||
lists.value = data.results
|
||||
nextUrl.value = data.next
|
||||
lists.value = data.results;
|
||||
nextUrl.value = data.next;
|
||||
} else if (data.results === 0) {
|
||||
error.value = STRINGS['checkinlist.none']
|
||||
error.value = STRINGS["checkinlist.none"];
|
||||
} else {
|
||||
error.value = data
|
||||
error.value = data;
|
||||
}
|
||||
} catch (e) {
|
||||
loading.value = false
|
||||
error.value = e
|
||||
loading.value = false;
|
||||
error.value = e;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNext () {
|
||||
if (!nextUrl.value) return
|
||||
async function loadNext() {
|
||||
if (!nextUrl.value) return;
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const data = await api.fetchNextPage<CheckinList>(nextUrl.value)
|
||||
loading.value = false
|
||||
const data = await api.fetchNextPage<CheckinList>(nextUrl.value);
|
||||
loading.value = false;
|
||||
|
||||
if (data.results) {
|
||||
lists.value.push(...data.results)
|
||||
nextUrl.value = data.next
|
||||
lists.value.push(...data.results);
|
||||
nextUrl.value = data.next;
|
||||
} else if (data.results === 0) {
|
||||
error.value = STRINGS['checkinlist.none']
|
||||
error.value = STRINGS["checkinlist.none"];
|
||||
} else {
|
||||
error.value = data
|
||||
error.value = data;
|
||||
}
|
||||
} catch (e) {
|
||||
loading.value = false
|
||||
error.value = e
|
||||
loading.value = false;
|
||||
error.value = e;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
load()
|
||||
})
|
||||
load();
|
||||
});
|
||||
</script>
|
||||
<template lang="pug">
|
||||
.panel.panel-primary.checkinlist-select
|
||||
|
||||
+41
-35
@@ -1,18 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { dateFormat, datetimeLocale } from '../i18n'
|
||||
import { ref, watch, onMounted, onUnmounted } from "vue";
|
||||
import { dateFormat, datetimeLocale } from "../i18n";
|
||||
|
||||
const props = defineProps<{
|
||||
required?: boolean
|
||||
modelValue?: string
|
||||
id?: string
|
||||
}>()
|
||||
required?: boolean;
|
||||
modelValue?: string;
|
||||
id?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
"update:modelValue": [value: string];
|
||||
}>();
|
||||
|
||||
const input = ref<HTMLInputElement>()
|
||||
const input = ref<HTMLInputElement>();
|
||||
|
||||
const opts = {
|
||||
format: dateFormat,
|
||||
@@ -20,44 +20,50 @@ const opts = {
|
||||
useCurrent: false,
|
||||
showClear: props.required,
|
||||
icons: {
|
||||
time: 'fa fa-clock-o',
|
||||
date: 'fa fa-calendar',
|
||||
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',
|
||||
time: "fa fa-clock-o",
|
||||
date: "fa fa-calendar",
|
||||
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(() => props.modelValue, (val) => {
|
||||
if (val) {
|
||||
$(input.value!).data('DateTimePicker').date(moment(val))
|
||||
}
|
||||
})
|
||||
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'))
|
||||
})
|
||||
.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))
|
||||
$(input.value!)
|
||||
.data("DateTimePicker")
|
||||
.viewDate(moment().hour(0).minute(0).second(0).millisecond(0));
|
||||
} else {
|
||||
$(input.value!).data('DateTimePicker').date(moment(props.modelValue))
|
||||
$(input.value!).data("DateTimePicker").date(moment(props.modelValue));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
$(input.value!)
|
||||
.off()
|
||||
.datetimepicker('destroy')
|
||||
})
|
||||
$(input.value!).off().datetimepicker("destroy");
|
||||
});
|
||||
</script>
|
||||
<template lang="pug">
|
||||
input.form-control(:id="id", ref="input", :required="required")
|
||||
|
||||
+41
-35
@@ -1,18 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { datetimeFormat, datetimeLocale, timezone } from '../i18n'
|
||||
import { ref, watch, onMounted, onUnmounted } from "vue";
|
||||
import { datetimeFormat, datetimeLocale, timezone } from "../i18n";
|
||||
|
||||
const props = defineProps<{
|
||||
required?: boolean
|
||||
modelValue?: string
|
||||
id?: string
|
||||
}>()
|
||||
required?: boolean;
|
||||
modelValue?: string;
|
||||
id?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
"update:modelValue": [value: string];
|
||||
}>();
|
||||
|
||||
const input = ref<HTMLInputElement>()
|
||||
const input = ref<HTMLInputElement>();
|
||||
|
||||
const opts = {
|
||||
format: datetimeFormat,
|
||||
@@ -21,44 +21,50 @@ const opts = {
|
||||
useCurrent: false,
|
||||
showClear: props.required,
|
||||
icons: {
|
||||
time: 'fa fa-clock-o',
|
||||
date: 'fa fa-calendar',
|
||||
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',
|
||||
time: "fa fa-clock-o",
|
||||
date: "fa fa-calendar",
|
||||
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(() => props.modelValue, (val) => {
|
||||
if (val) {
|
||||
$(input.value!).data('DateTimePicker').date(moment(val))
|
||||
}
|
||||
})
|
||||
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())
|
||||
})
|
||||
.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))
|
||||
$(input.value!)
|
||||
.data("DateTimePicker")
|
||||
.viewDate(moment().hour(0).minute(0).second(0).millisecond(0));
|
||||
} else {
|
||||
$(input.value!).data('DateTimePicker').date(moment(props.modelValue))
|
||||
$(input.value!).data("DateTimePicker").date(moment(props.modelValue));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
$(input.value!)
|
||||
.off()
|
||||
.datetimepicker('destroy')
|
||||
})
|
||||
$(input.value!).off().datetimepicker("destroy");
|
||||
});
|
||||
</script>
|
||||
<template lang="pug">
|
||||
input.form-control(:id="id", ref="input", :required="required")
|
||||
|
||||
+26
-18
@@ -1,35 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import type { Position } from '../api'
|
||||
import { STRINGS, i18nstringLocalize, formatSubevent } from '../i18n'
|
||||
import { computed, ref } from "vue";
|
||||
import type { Position } from "../api";
|
||||
import { STRINGS, i18nstringLocalize, formatSubevent } from "../i18n";
|
||||
|
||||
const props = defineProps<{
|
||||
position: Position
|
||||
}>()
|
||||
position: Position;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
selected: [position: Position]
|
||||
}>()
|
||||
selected: [position: Position];
|
||||
}>();
|
||||
|
||||
const rootEl = ref<HTMLAnchorElement>()
|
||||
const rootEl = ref<HTMLAnchorElement>();
|
||||
|
||||
const status = computed(() => {
|
||||
if (props.position.checkins.length) return 'redeemed'
|
||||
if (props.position.order__status === 'n' && props.position.order__valid_if_pending) return 'pending_valid'
|
||||
if (props.position.order__status === 'n' && props.position.order__require_approval) return 'require_approval'
|
||||
return props.position.order__status
|
||||
})
|
||||
if (props.position.checkins.length) return "redeemed";
|
||||
if (
|
||||
props.position.order__status === "n" &&
|
||||
props.position.order__valid_if_pending
|
||||
)
|
||||
return "pending_valid";
|
||||
if (
|
||||
props.position.order__status === "n" &&
|
||||
props.position.order__require_approval
|
||||
)
|
||||
return "require_approval";
|
||||
return props.position.order__status;
|
||||
});
|
||||
|
||||
const itemvar = computed(() => {
|
||||
if (props.position.variation) {
|
||||
return `${i18nstringLocalize(props.position.item.name)} – ${i18nstringLocalize(props.position.variation.value)}`
|
||||
return `${i18nstringLocalize(props.position.item.name)} – ${i18nstringLocalize(props.position.variation.value)}`;
|
||||
}
|
||||
return i18nstringLocalize(props.position.item.name)
|
||||
})
|
||||
return i18nstringLocalize(props.position.item.name);
|
||||
});
|
||||
|
||||
const subevent = computed(() => formatSubevent(props.position.subevent))
|
||||
const subevent = computed(() => formatSubevent(props.position.subevent));
|
||||
|
||||
defineExpose({ el: rootEl })
|
||||
defineExpose({ el: rootEl });
|
||||
</script>
|
||||
<template lang="pug">
|
||||
a.list-group-item.searchresult(ref="rootEl", href="#", @click.prevent="$emit('selected', position)")
|
||||
|
||||
+41
-35
@@ -1,18 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { timeFormat, datetimeLocale } from '../i18n'
|
||||
import { ref, watch, onMounted, onUnmounted } from "vue";
|
||||
import { timeFormat, datetimeLocale } from "../i18n";
|
||||
|
||||
const props = defineProps<{
|
||||
required?: boolean
|
||||
modelValue?: string
|
||||
id?: string
|
||||
}>()
|
||||
required?: boolean;
|
||||
modelValue?: string;
|
||||
id?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
"update:modelValue": [value: string];
|
||||
}>();
|
||||
|
||||
const input = ref<HTMLInputElement>()
|
||||
const input = ref<HTMLInputElement>();
|
||||
|
||||
const opts = {
|
||||
format: timeFormat,
|
||||
@@ -20,44 +20,50 @@ const opts = {
|
||||
useCurrent: false,
|
||||
showClear: props.required,
|
||||
icons: {
|
||||
time: 'fa fa-clock-o',
|
||||
date: 'fa fa-calendar',
|
||||
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',
|
||||
time: "fa fa-clock-o",
|
||||
date: "fa fa-calendar",
|
||||
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(() => props.modelValue, (val) => {
|
||||
if (val) {
|
||||
$(input.value!).data('DateTimePicker').date(moment(val))
|
||||
}
|
||||
})
|
||||
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'))
|
||||
})
|
||||
.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))
|
||||
$(input.value!)
|
||||
.data("DateTimePicker")
|
||||
.viewDate(moment().hour(0).minute(0).second(0).millisecond(0));
|
||||
} else {
|
||||
$(input.value!).data('DateTimePicker').date(moment(props.modelValue))
|
||||
$(input.value!).data("DateTimePicker").date(moment(props.modelValue));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
$(input.value!)
|
||||
.off()
|
||||
.datetimepicker('destroy')
|
||||
})
|
||||
$(input.value!).off().datetimepicker("destroy");
|
||||
});
|
||||
</script>
|
||||
<template lang="pug">
|
||||
input.form-control(:id="id", ref="input", :required="required")
|
||||
|
||||
@@ -1,106 +1,115 @@
|
||||
const body = document.body
|
||||
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'
|
||||
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)
|
||||
moment.locale(datetimeLocale);
|
||||
|
||||
export function gettext (msgid: string): string {
|
||||
if (typeof django !== 'undefined' && typeof django.gettext !== 'undefined') {
|
||||
return django.gettext(msgid)
|
||||
export function gettext(msgid: string): string {
|
||||
if (typeof django !== "undefined" && typeof django.gettext !== "undefined") {
|
||||
return django.gettext(msgid);
|
||||
}
|
||||
return 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)
|
||||
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
|
||||
return plural;
|
||||
}
|
||||
|
||||
export type I18nString = string | Record<string, string> | null | undefined
|
||||
export type I18nString = string | Record<string, string> | null | undefined;
|
||||
|
||||
export function i18nstringLocalize (obj: I18nString): string {
|
||||
export function i18nstringLocalize(obj: I18nString): string {
|
||||
// external
|
||||
return i18nstring_localize(obj)
|
||||
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'),
|
||||
}
|
||||
"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
|
||||
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 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
|
||||
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')
|
||||
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
|
||||
return value;
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createApp } from "vue";
|
||||
|
||||
// import './scss/main.scss'
|
||||
|
||||
import App from './components/app.vue'
|
||||
import App from "./components/app.vue";
|
||||
|
||||
const mountEl = document.querySelector<HTMLElement>('#app')!
|
||||
const mountEl = document.querySelector<HTMLElement>("#app")!;
|
||||
|
||||
const app = createApp(App, mountEl.dataset)
|
||||
app.mount('#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)
|
||||
}
|
||||
console.error("[VUE]", info, error);
|
||||
};
|
||||
@@ -75,6 +75,7 @@ class AuthenticationForm(forms.Form):
|
||||
'unverified': _("You have not yet activated your account and set a password. Please click the link in the "
|
||||
"email we sent you. In case you cannot find it, click \"Forgot your password?\" to receive "
|
||||
"a new email."),
|
||||
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
|
||||
}
|
||||
|
||||
def __init__(self, request=None, *args, **kwargs):
|
||||
@@ -93,6 +94,12 @@ class AuthenticationForm(forms.Form):
|
||||
email = self.cleaned_data.get('email')
|
||||
password = self.cleaned_data.get('password')
|
||||
|
||||
if rate_limit("customer_login", include_ip_from_request=self.request, max_num=10, expire_time=300):
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
|
||||
if email is not None and password:
|
||||
try:
|
||||
u = self.request.organizer.customers.get(email=email.lower(), provider__isnull=True)
|
||||
|
||||
@@ -1,72 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { rules as rawRules, allItems, activeItems, allProducts, limitProducts } from './django-interop'
|
||||
import { convertToDNF } from './jsonlogic-boolalg'
|
||||
import { computed } from "vue";
|
||||
import {
|
||||
rules as rawRules,
|
||||
allItems,
|
||||
activeItems,
|
||||
allProducts,
|
||||
limitProducts,
|
||||
} from "./django-interop";
|
||||
import { convertToDNF } from "./jsonlogic-boolalg";
|
||||
|
||||
import RulesEditor from './checkin-rules-editor.vue'
|
||||
import RulesVisualization from './checkin-rules-visualization.vue'
|
||||
import RulesEditor from "./checkin-rules-editor.vue";
|
||||
import RulesVisualization from "./checkin-rules-visualization.vue";
|
||||
|
||||
const gettext = (window as any).gettext
|
||||
const gettext = (window as any).gettext;
|
||||
|
||||
const missingItems = computed(() => {
|
||||
// This computed variable contains list of item or variation names that
|
||||
// a) Are allowed on the checkin list according to all_products or include_products
|
||||
// b) Are not matched by ANY logical branch of the rule.
|
||||
// The list will be empty if there is a "catch-all" rule.
|
||||
let productsSeen = {}
|
||||
let variationsSeen = {}
|
||||
let rules = convertToDNF(rawRules.value)
|
||||
let branchWithoutProductFilter = false
|
||||
let productsSeen = {};
|
||||
let variationsSeen = {};
|
||||
let rules = convertToDNF(rawRules.value);
|
||||
let branchWithoutProductFilter = false;
|
||||
|
||||
if (!rules.or) {
|
||||
rules = { or: [rules] }
|
||||
rules = { or: [rules] };
|
||||
}
|
||||
|
||||
for (let part of rules.or) {
|
||||
if (!part.and) {
|
||||
part = { and: [part] }
|
||||
part = { and: [part] };
|
||||
}
|
||||
let thisBranchWithoutProductFilter = true
|
||||
let thisBranchWithoutProductFilter = true;
|
||||
for (let subpart of part.and) {
|
||||
if (subpart.inList) {
|
||||
if (subpart.inList[0].var === 'product' && subpart.inList[1]) {
|
||||
thisBranchWithoutProductFilter = false
|
||||
if (subpart.inList[0].var === "product" && subpart.inList[1]) {
|
||||
thisBranchWithoutProductFilter = false;
|
||||
for (let listentry of subpart.inList[1].objectList) {
|
||||
productsSeen[parseInt(listentry.lookup[1])] = true
|
||||
productsSeen[parseInt(listentry.lookup[1])] = true;
|
||||
}
|
||||
} else if (subpart.inList[0].var === 'variation' && subpart.inList[1]) {
|
||||
thisBranchWithoutProductFilter = false
|
||||
} else if (subpart.inList[0].var === "variation" && subpart.inList[1]) {
|
||||
thisBranchWithoutProductFilter = false;
|
||||
for (let listentry_ of subpart.inList[1].objectList) {
|
||||
variationsSeen[parseInt(listentry_.lookup[1])] = true
|
||||
variationsSeen[parseInt(listentry_.lookup[1])] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (thisBranchWithoutProductFilter) {
|
||||
branchWithoutProductFilter = true
|
||||
break
|
||||
branchWithoutProductFilter = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (branchWithoutProductFilter || (!Object.keys(productsSeen).length && !Object.keys(variationsSeen).length)) {
|
||||
if (
|
||||
branchWithoutProductFilter ||
|
||||
(!Object.keys(productsSeen).length && !Object.keys(variationsSeen).length)
|
||||
) {
|
||||
// At least one branch with no product filters at all – that's fine.
|
||||
return []
|
||||
return [];
|
||||
}
|
||||
|
||||
let missing = []
|
||||
let missing = [];
|
||||
for (const item of activeItems.value) {
|
||||
if (productsSeen[item.id]) continue
|
||||
if (!allProducts.value && !limitProducts.value.includes(item.id)) continue
|
||||
if (productsSeen[item.id]) continue;
|
||||
if (!allProducts.value && !limitProducts.value.includes(item.id)) continue;
|
||||
if (item.variations.length > 0) {
|
||||
for (let variation of item.variations) {
|
||||
if (variationsSeen[variation.id]) continue
|
||||
missing.push(item.name + ' – ' + variation.name)
|
||||
if (variationsSeen[variation.id]) continue;
|
||||
missing.push(item.name + " – " + variation.name);
|
||||
}
|
||||
} else {
|
||||
missing.push(item.name)
|
||||
missing.push(item.name);
|
||||
}
|
||||
}
|
||||
return missing
|
||||
})
|
||||
return missing;
|
||||
});
|
||||
</script>
|
||||
<template lang="pug">
|
||||
#rules-editor.form-inline
|
||||
@@ -98,5 +107,4 @@ const missingItems = computed(() => {
|
||||
li(v-for="h in missingItems", :key="h") {{ h }}
|
||||
p {{ gettext("Please double-check if this was intentional.") }}
|
||||
</template>
|
||||
<style lang="stylus">
|
||||
</style>
|
||||
<style lang="stylus"></style>
|
||||
@@ -1,230 +1,258 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable vue/no-mutating-props */
|
||||
import { computed } from 'vue'
|
||||
import { TEXTS, VARS, TYPEOPS } from './constants'
|
||||
import { productSelectURL, variationSelectURL, gateSelectURL } from './django-interop'
|
||||
import LookupSelect2 from './lookup-select2.vue'
|
||||
import Datetimefield from './datetimefield.vue'
|
||||
import Timefield from './timefield.vue'
|
||||
import { computed } from "vue";
|
||||
import { TEXTS, VARS, TYPEOPS } from "./constants";
|
||||
import {
|
||||
productSelectURL,
|
||||
variationSelectURL,
|
||||
gateSelectURL,
|
||||
} from "./django-interop";
|
||||
import LookupSelect2 from "./lookup-select2.vue";
|
||||
import Datetimefield from "./datetimefield.vue";
|
||||
import Timefield from "./timefield.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
rule: any
|
||||
level: number
|
||||
index: number
|
||||
}>()
|
||||
rule: any;
|
||||
level: number;
|
||||
index: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
remove: []
|
||||
duplicate: []
|
||||
}>()
|
||||
remove: [];
|
||||
duplicate: [];
|
||||
}>();
|
||||
|
||||
const operator = computed(() => Object.keys(props.rule)[0])
|
||||
const operands = computed(() => props.rule[operator.value])
|
||||
const operator = computed(() => Object.keys(props.rule)[0]);
|
||||
const operands = computed(() => props.rule[operator.value]);
|
||||
|
||||
const variable = computed(() => {
|
||||
const op = operator.value
|
||||
if (op === 'and' || op === 'or') {
|
||||
return op
|
||||
const op = operator.value;
|
||||
if (op === "and" || op === "or") {
|
||||
return op;
|
||||
} else if (props.rule[op]?.[0]) {
|
||||
if (props.rule[op][0]['entries_since']) return 'entries_since'
|
||||
if (props.rule[op][0]['entries_before']) return 'entries_before'
|
||||
if (props.rule[op][0]['entries_days_since']) return 'entries_days_since'
|
||||
if (props.rule[op][0]['entries_days_before']) return 'entries_days_before'
|
||||
return props.rule[op][0]['var']
|
||||
if (props.rule[op][0]["entries_since"]) return "entries_since";
|
||||
if (props.rule[op][0]["entries_before"]) return "entries_before";
|
||||
if (props.rule[op][0]["entries_days_since"]) return "entries_days_since";
|
||||
if (props.rule[op][0]["entries_days_before"]) return "entries_days_before";
|
||||
return props.rule[op][0]["var"];
|
||||
}
|
||||
return null
|
||||
})
|
||||
return null;
|
||||
});
|
||||
|
||||
const rightoperand = computed(() => {
|
||||
const op = operator.value
|
||||
if (op === 'and' || op === 'or') return null
|
||||
return props.rule[op]?.[1] ?? null
|
||||
})
|
||||
const op = operator.value;
|
||||
if (op === "and" || op === "or") return null;
|
||||
return props.rule[op]?.[1] ?? null;
|
||||
});
|
||||
|
||||
const classObject = computed(() => ({
|
||||
'checkin-rule': true,
|
||||
['checkin-rule-' + variable.value]: true
|
||||
}))
|
||||
"checkin-rule": true,
|
||||
["checkin-rule-" + variable.value]: true,
|
||||
}));
|
||||
|
||||
const vartype = computed(() => VARS[variable.value]?.type)
|
||||
const vartype = computed(() => VARS[variable.value]?.type);
|
||||
|
||||
const timeType = computed(() => {
|
||||
if (vartype.value === 'int_by_datetime') {
|
||||
return props.rule[operator.value]?.[0]?.[variable.value]?.[0]?.buildTime?.[0]
|
||||
if (vartype.value === "int_by_datetime") {
|
||||
return props.rule[operator.value]?.[0]?.[variable.value]?.[0]
|
||||
?.buildTime?.[0];
|
||||
}
|
||||
return rightoperand.value?.buildTime?.[0]
|
||||
})
|
||||
return rightoperand.value?.buildTime?.[0];
|
||||
});
|
||||
|
||||
const timeTolerance = computed(() => {
|
||||
const op = operator.value
|
||||
if ((op === 'isBefore' || op === 'isAfter') && props.rule[op]?.[2] !== undefined) {
|
||||
return props.rule[op][2]
|
||||
const op = operator.value;
|
||||
if (
|
||||
(op === "isBefore" || op === "isAfter") &&
|
||||
props.rule[op]?.[2] !== undefined
|
||||
) {
|
||||
return props.rule[op][2];
|
||||
}
|
||||
return null
|
||||
})
|
||||
return null;
|
||||
});
|
||||
|
||||
const timeValue = computed(() => {
|
||||
if (vartype.value === 'int_by_datetime') {
|
||||
return props.rule[operator.value]?.[0]?.[variable.value]?.[0]?.buildTime?.[1]
|
||||
if (vartype.value === "int_by_datetime") {
|
||||
return props.rule[operator.value]?.[0]?.[variable.value]?.[0]
|
||||
?.buildTime?.[1];
|
||||
}
|
||||
return rightoperand.value?.buildTime?.[1]
|
||||
})
|
||||
return rightoperand.value?.buildTime?.[1];
|
||||
});
|
||||
|
||||
const cardinality = computed(() => TYPEOPS[vartype.value]?.[operator.value]?.cardinality)
|
||||
const operators = computed(() => TYPEOPS[vartype.value])
|
||||
const cardinality = computed(
|
||||
() => TYPEOPS[vartype.value]?.[operator.value]?.cardinality,
|
||||
);
|
||||
const operators = computed(() => TYPEOPS[vartype.value]);
|
||||
|
||||
function setVariable (event: Event) {
|
||||
const target = event.target as HTMLSelectElement
|
||||
const currentOp = Object.keys(props.rule)[0]
|
||||
let currentVal = props.rule[currentOp]
|
||||
function setVariable(event: Event) {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
const currentOp = Object.keys(props.rule)[0];
|
||||
let currentVal = props.rule[currentOp];
|
||||
|
||||
if (target.value === 'and' || target.value === 'or') {
|
||||
if (currentVal[0]?.var) currentVal = []
|
||||
props.rule[target.value] = currentVal
|
||||
delete props.rule[currentOp]
|
||||
if (target.value === "and" || target.value === "or") {
|
||||
if (currentVal[0]?.var) currentVal = [];
|
||||
props.rule[target.value] = currentVal;
|
||||
delete props.rule[currentOp];
|
||||
} else {
|
||||
if (currentVal !== 'and' && currentVal !== 'or' && currentVal[0] && VARS[target.value]?.type === vartype.value) {
|
||||
if (vartype.value === 'int_by_datetime') {
|
||||
const currentData = props.rule[currentOp][0][variable.value]
|
||||
props.rule[currentOp][0] = { [target.value]: JSON.parse(JSON.stringify(currentData)) }
|
||||
if (
|
||||
currentVal !== "and" &&
|
||||
currentVal !== "or" &&
|
||||
currentVal[0] &&
|
||||
VARS[target.value]?.type === vartype.value
|
||||
) {
|
||||
if (vartype.value === "int_by_datetime") {
|
||||
const currentData = props.rule[currentOp][0][variable.value];
|
||||
props.rule[currentOp][0] = {
|
||||
[target.value]: JSON.parse(JSON.stringify(currentData)),
|
||||
};
|
||||
} else {
|
||||
props.rule[currentOp][0].var = target.value
|
||||
props.rule[currentOp][0].var = target.value;
|
||||
}
|
||||
} else if (VARS[target.value]?.type === 'int_by_datetime') {
|
||||
delete props.rule[currentOp]
|
||||
props.rule['!!'] = [{ [target.value]: [{ buildTime: [null, null] }] }]
|
||||
} else if (VARS[target.value]?.type === "int_by_datetime") {
|
||||
delete props.rule[currentOp];
|
||||
props.rule["!!"] = [{ [target.value]: [{ buildTime: [null, null] }] }];
|
||||
} else {
|
||||
delete props.rule[currentOp]
|
||||
props.rule['!!'] = [{ var: target.value }]
|
||||
delete props.rule[currentOp];
|
||||
props.rule["!!"] = [{ var: target.value }];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setOperator (event: Event) {
|
||||
const target = event.target as HTMLSelectElement
|
||||
const currentOp = Object.keys(props.rule)[0]
|
||||
const currentVal = props.rule[currentOp]
|
||||
delete props.rule[currentOp]
|
||||
props.rule[target.value] = currentVal
|
||||
function setOperator(event: Event) {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
const currentOp = Object.keys(props.rule)[0];
|
||||
const currentVal = props.rule[currentOp];
|
||||
delete props.rule[currentOp];
|
||||
props.rule[target.value] = currentVal;
|
||||
}
|
||||
|
||||
function setRightOperandNumber (event: Event) {
|
||||
const val = parseInt((event.target as HTMLInputElement).value)
|
||||
function setRightOperandNumber(event: Event) {
|
||||
const val = parseInt((event.target as HTMLInputElement).value);
|
||||
if (props.rule[operator.value].length === 1) {
|
||||
props.rule[operator.value].push(val)
|
||||
props.rule[operator.value].push(val);
|
||||
} else {
|
||||
props.rule[operator.value][1] = val
|
||||
props.rule[operator.value][1] = val;
|
||||
}
|
||||
}
|
||||
|
||||
function setTimeTolerance (event: Event) {
|
||||
const val = parseInt((event.target as HTMLInputElement).value)
|
||||
function setTimeTolerance(event: Event) {
|
||||
const val = parseInt((event.target as HTMLInputElement).value);
|
||||
if (props.rule[operator.value].length === 2) {
|
||||
props.rule[operator.value].push(val)
|
||||
props.rule[operator.value].push(val);
|
||||
} else {
|
||||
props.rule[operator.value][2] = val
|
||||
props.rule[operator.value][2] = val;
|
||||
}
|
||||
}
|
||||
|
||||
function setTimeType (event: Event) {
|
||||
const val = (event.target as HTMLSelectElement).value
|
||||
const time = { buildTime: [val] }
|
||||
if (vartype.value === 'int_by_datetime') {
|
||||
props.rule[operator.value][0][variable.value][0] = time
|
||||
function setTimeType(event: Event) {
|
||||
const val = (event.target as HTMLSelectElement).value;
|
||||
const time = { buildTime: [val] };
|
||||
if (vartype.value === "int_by_datetime") {
|
||||
props.rule[operator.value][0][variable.value][0] = time;
|
||||
} else {
|
||||
if (props.rule[operator.value].length === 1) {
|
||||
props.rule[operator.value].push(time)
|
||||
props.rule[operator.value].push(time);
|
||||
} else {
|
||||
props.rule[operator.value][1] = time
|
||||
props.rule[operator.value][1] = time;
|
||||
}
|
||||
if (val === 'custom') {
|
||||
props.rule[operator.value][2] = 0
|
||||
if (val === "custom") {
|
||||
props.rule[operator.value][2] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setTimeValue (val: string) {
|
||||
if (vartype.value === 'int_by_datetime') {
|
||||
props.rule[operator.value][0][variable.value][0]['buildTime'][1] = val
|
||||
function setTimeValue(val: string) {
|
||||
if (vartype.value === "int_by_datetime") {
|
||||
props.rule[operator.value][0][variable.value][0]["buildTime"][1] = val;
|
||||
} else {
|
||||
props.rule[operator.value][1]['buildTime'][1] = val
|
||||
props.rule[operator.value][1]["buildTime"][1] = val;
|
||||
}
|
||||
}
|
||||
|
||||
function setRightOperandProductList (val: { id: any; text: string }[]) {
|
||||
const products = { objectList: val.map(item => ({ lookup: ['product', item.id, item.text] })) }
|
||||
function setRightOperandProductList(val: { id: any; text: string }[]) {
|
||||
const products = {
|
||||
objectList: val.map((item) => ({
|
||||
lookup: ["product", item.id, item.text],
|
||||
})),
|
||||
};
|
||||
if (props.rule[operator.value].length === 1) {
|
||||
props.rule[operator.value].push(products)
|
||||
props.rule[operator.value].push(products);
|
||||
} else {
|
||||
props.rule[operator.value][1] = products
|
||||
props.rule[operator.value][1] = products;
|
||||
}
|
||||
}
|
||||
|
||||
function setRightOperandVariationList (val: { id: any; text: string }[]) {
|
||||
const products = { objectList: val.map(item => ({ lookup: ['variation', item.id, item.text] })) }
|
||||
function setRightOperandVariationList(val: { id: any; text: string }[]) {
|
||||
const products = {
|
||||
objectList: val.map((item) => ({
|
||||
lookup: ["variation", item.id, item.text],
|
||||
})),
|
||||
};
|
||||
if (props.rule[operator.value].length === 1) {
|
||||
props.rule[operator.value].push(products)
|
||||
props.rule[operator.value].push(products);
|
||||
} else {
|
||||
props.rule[operator.value][1] = products
|
||||
props.rule[operator.value][1] = products;
|
||||
}
|
||||
}
|
||||
|
||||
function setRightOperandGateList (val: { id: any; text: string }[]) {
|
||||
const products = { objectList: val.map(item => ({ lookup: ['gate', item.id, item.text] })) }
|
||||
function setRightOperandGateList(val: { id: any; text: string }[]) {
|
||||
const products = {
|
||||
objectList: val.map((item) => ({ lookup: ["gate", item.id, item.text] })),
|
||||
};
|
||||
if (props.rule[operator.value].length === 1) {
|
||||
props.rule[operator.value].push(products)
|
||||
props.rule[operator.value].push(products);
|
||||
} else {
|
||||
props.rule[operator.value][1] = products
|
||||
props.rule[operator.value][1] = products;
|
||||
}
|
||||
}
|
||||
|
||||
function setRightOperandEnum (event: Event) {
|
||||
const val = (event.target as HTMLSelectElement).value
|
||||
function setRightOperandEnum(event: Event) {
|
||||
const val = (event.target as HTMLSelectElement).value;
|
||||
if (props.rule[operator.value].length === 1) {
|
||||
props.rule[operator.value].push(val)
|
||||
props.rule[operator.value].push(val);
|
||||
} else {
|
||||
props.rule[operator.value][1] = val
|
||||
props.rule[operator.value][1] = val;
|
||||
}
|
||||
}
|
||||
|
||||
function addOperand () {
|
||||
props.rule[operator.value].push({ '': [] })
|
||||
function addOperand() {
|
||||
props.rule[operator.value].push({ "": [] });
|
||||
}
|
||||
|
||||
function wrapWithOR () {
|
||||
const r = JSON.parse(JSON.stringify(props.rule))
|
||||
delete props.rule[operator.value]
|
||||
props.rule.or = [r]
|
||||
function wrapWithOR() {
|
||||
const r = JSON.parse(JSON.stringify(props.rule));
|
||||
delete props.rule[operator.value];
|
||||
props.rule.or = [r];
|
||||
}
|
||||
|
||||
function wrapWithAND () {
|
||||
const r = JSON.parse(JSON.stringify(props.rule))
|
||||
delete props.rule[operator.value]
|
||||
props.rule.and = [r]
|
||||
function wrapWithAND() {
|
||||
const r = JSON.parse(JSON.stringify(props.rule));
|
||||
delete props.rule[operator.value];
|
||||
props.rule.and = [r];
|
||||
}
|
||||
|
||||
function cutOut () {
|
||||
const cop = Object.keys(operands.value[0])[0]
|
||||
const r = operands.value[0][cop]
|
||||
delete props.rule[operator.value]
|
||||
props.rule[cop] = r
|
||||
function cutOut() {
|
||||
const cop = Object.keys(operands.value[0])[0];
|
||||
const r = operands.value[0][cop];
|
||||
delete props.rule[operator.value];
|
||||
props.rule[cop] = r;
|
||||
}
|
||||
|
||||
function remove () {
|
||||
emit('remove')
|
||||
function remove() {
|
||||
emit("remove");
|
||||
}
|
||||
|
||||
function duplicate () {
|
||||
emit('duplicate')
|
||||
function duplicate() {
|
||||
emit("duplicate");
|
||||
}
|
||||
|
||||
function removeChild (index: number) {
|
||||
props.rule[operator.value].splice(index, 1)
|
||||
function removeChild(index: number) {
|
||||
props.rule[operator.value].splice(index, 1);
|
||||
}
|
||||
|
||||
function duplicateChild (index: number) {
|
||||
const r = JSON.parse(JSON.stringify(props.rule[operator.value][index]))
|
||||
props.rule[operator.value].splice(index, 0, r)
|
||||
function duplicateChild(index: number) {
|
||||
const r = JSON.parse(JSON.stringify(props.rule[operator.value][index]));
|
||||
props.rule[operator.value].splice(index, 0, r);
|
||||
}
|
||||
</script>
|
||||
<template lang="pug">
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { TEXTS } from './constants'
|
||||
import { rules } from './django-interop'
|
||||
import CheckinRule from './checkin-rule.vue'
|
||||
import { computed } from "vue";
|
||||
import { TEXTS } from "./constants";
|
||||
import { rules } from "./django-interop";
|
||||
import CheckinRule from "./checkin-rule.vue";
|
||||
|
||||
const hasRules = computed(() => !!Object.keys(rules.value).length)
|
||||
const hasRules = computed(() => !!Object.keys(rules.value).length);
|
||||
|
||||
function addRule () {
|
||||
rules.value.and = []
|
||||
function addRule() {
|
||||
rules.value.and = [];
|
||||
}
|
||||
</script>
|
||||
<template lang="pug">
|
||||
|
||||
+137
-114
@@ -1,36 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { rules } from './django-interop'
|
||||
import VizNode from './viz-node.vue'
|
||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from "vue";
|
||||
import { rules } from "./django-interop";
|
||||
import VizNode from "./viz-node.vue";
|
||||
|
||||
declare const d3: any
|
||||
declare const d3: any;
|
||||
|
||||
const svg = ref<SVGSVGElement | null>(null)
|
||||
const maximized = ref(false)
|
||||
const zoom = ref<any>(null)
|
||||
const defaultScale = ref(1)
|
||||
const zoomTransform = ref(d3.zoomTransform({ k: 1, x: 0, y: 0 }))
|
||||
const svg = ref<SVGSVGElement | null>(null);
|
||||
const maximized = ref(false);
|
||||
const zoom = ref<any>(null);
|
||||
const defaultScale = ref(1);
|
||||
const zoomTransform = ref(d3.zoomTransform({ k: 1, x: 0, y: 0 }));
|
||||
|
||||
const boxWidth = 300
|
||||
const boxHeight = 62
|
||||
const paddingX = 50
|
||||
const marginX = 50
|
||||
const marginY = 20
|
||||
const boxWidth = 300;
|
||||
const boxHeight = 62;
|
||||
const paddingX = 50;
|
||||
const marginX = 50;
|
||||
const marginY = 20;
|
||||
|
||||
interface GraphNode {
|
||||
rule: any
|
||||
column: number
|
||||
children: string[]
|
||||
y?: number
|
||||
parent?: GraphNode
|
||||
rule: any;
|
||||
column: number;
|
||||
children: string[];
|
||||
y?: number;
|
||||
parent?: GraphNode;
|
||||
}
|
||||
|
||||
interface Graph {
|
||||
nodes_by_id: Record<string, GraphNode>
|
||||
children: string[]
|
||||
columns: number
|
||||
height: number
|
||||
y?: number
|
||||
nodes_by_id: Record<string, GraphNode>;
|
||||
children: string[];
|
||||
columns: number;
|
||||
height: number;
|
||||
y?: number;
|
||||
}
|
||||
|
||||
const graph = computed<Graph>(() => {
|
||||
@@ -69,175 +69,198 @@ const graph = computed<Graph>(() => {
|
||||
children: [],
|
||||
columns: -1,
|
||||
height: 1,
|
||||
}
|
||||
};
|
||||
|
||||
// Step 1: Start building the graph by finding all nodes and edges
|
||||
let counter = 0
|
||||
const _add_to_graph = (rule: any): [string[], string[]] => { // returns [heads, tails]
|
||||
if (typeof rule !== 'object' || rule === null) {
|
||||
const node_id = (counter++).toString()
|
||||
let counter = 0;
|
||||
const _add_to_graph = (rule: any): [string[], string[]] => {
|
||||
// returns [heads, tails]
|
||||
if (typeof rule !== "object" || rule === null) {
|
||||
const node_id = (counter++).toString();
|
||||
graphData.nodes_by_id[node_id] = {
|
||||
rule: rule,
|
||||
column: -1,
|
||||
children: [],
|
||||
}
|
||||
return [[node_id], [node_id]]
|
||||
};
|
||||
return [[node_id], [node_id]];
|
||||
}
|
||||
|
||||
const operator = Object.keys(rule)[0]
|
||||
const operands = rule[operator]
|
||||
const operator = Object.keys(rule)[0];
|
||||
const operands = rule[operator];
|
||||
|
||||
if (operator === 'and') {
|
||||
let children: string[] = []
|
||||
let tails: string[] | null = null
|
||||
operands.reverse()
|
||||
if (operator === "and") {
|
||||
let children: string[] = [];
|
||||
let tails: string[] | null = null;
|
||||
operands.reverse();
|
||||
for (const operand of operands) {
|
||||
const [new_children, new_tails] = _add_to_graph(operand)
|
||||
const [new_children, new_tails] = _add_to_graph(operand);
|
||||
for (const new_child of new_tails) {
|
||||
graphData.nodes_by_id[new_child].children.push(...children)
|
||||
graphData.nodes_by_id[new_child].children.push(...children);
|
||||
for (const c of children) {
|
||||
graphData.nodes_by_id[c].parent = graphData.nodes_by_id[new_child]
|
||||
graphData.nodes_by_id[c].parent = graphData.nodes_by_id[new_child];
|
||||
}
|
||||
}
|
||||
if (tails === null) {
|
||||
tails = new_tails
|
||||
tails = new_tails;
|
||||
}
|
||||
children = new_children
|
||||
children = new_children;
|
||||
}
|
||||
return [children, tails!]
|
||||
} else if (operator === 'or') {
|
||||
const children: string[] = []
|
||||
const tails: string[] = []
|
||||
return [children, tails!];
|
||||
} else if (operator === "or") {
|
||||
const children: string[] = [];
|
||||
const tails: string[] = [];
|
||||
for (const operand of operands) {
|
||||
const [new_children, new_tails] = _add_to_graph(operand)
|
||||
children.push(...new_children)
|
||||
tails.push(...new_tails)
|
||||
const [new_children, new_tails] = _add_to_graph(operand);
|
||||
children.push(...new_children);
|
||||
tails.push(...new_tails);
|
||||
}
|
||||
return [children, tails]
|
||||
return [children, tails];
|
||||
} else {
|
||||
const node_id = (counter++).toString()
|
||||
const node_id = (counter++).toString();
|
||||
graphData.nodes_by_id[node_id] = {
|
||||
rule: rule,
|
||||
column: -1,
|
||||
children: [],
|
||||
}
|
||||
return [[node_id], [node_id]]
|
||||
};
|
||||
return [[node_id], [node_id]];
|
||||
}
|
||||
}
|
||||
graphData.children = _add_to_graph(JSON.parse(JSON.stringify(rules.value)))[0]
|
||||
};
|
||||
graphData.children = _add_to_graph(
|
||||
JSON.parse(JSON.stringify(rules.value)),
|
||||
)[0];
|
||||
|
||||
// Step 2: We compute the "column" of every node, which is the maximum number of hops required to reach the
|
||||
// node from the root node
|
||||
const _set_column_to_min = (nodes: GraphNode[], mincol: number) => {
|
||||
for (const node of nodes) {
|
||||
if (mincol > node.column) {
|
||||
node.column = mincol
|
||||
graphData.columns = Math.max(mincol + 1, graphData.columns)
|
||||
_set_column_to_min(node.children.map(nid => graphData.nodes_by_id[nid]), mincol + 1)
|
||||
node.column = mincol;
|
||||
graphData.columns = Math.max(mincol + 1, graphData.columns);
|
||||
_set_column_to_min(
|
||||
node.children.map((nid) => graphData.nodes_by_id[nid]),
|
||||
mincol + 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_set_column_to_min(graphData.children.map(nid => graphData.nodes_by_id[nid]), 0)
|
||||
};
|
||||
_set_column_to_min(
|
||||
graphData.children.map((nid) => graphData.nodes_by_id[nid]),
|
||||
0,
|
||||
);
|
||||
|
||||
// Step 3: Align each node on a grid. The x position is already given by the column computed above, but we still
|
||||
// need the y position. This part of the algorithm is opinionated and probably not yet the nicest solution we
|
||||
// can use!
|
||||
const _set_y = (node: Graph | GraphNode, offset: number): number => {
|
||||
if (typeof node.y === 'undefined') {
|
||||
if (typeof node.y === "undefined") {
|
||||
// We only take the first value we found for each node
|
||||
node.y = offset
|
||||
node.y = offset;
|
||||
}
|
||||
|
||||
let used = 0
|
||||
let used = 0;
|
||||
for (const cid of node.children) {
|
||||
used += Math.max(0, _set_y(graphData.nodes_by_id[cid], offset + used) - 1)
|
||||
used++
|
||||
used += Math.max(
|
||||
0,
|
||||
_set_y(graphData.nodes_by_id[cid], offset + used) - 1,
|
||||
);
|
||||
used++;
|
||||
}
|
||||
return used
|
||||
}
|
||||
_set_y(graphData, 0)
|
||||
return used;
|
||||
};
|
||||
_set_y(graphData, 0);
|
||||
|
||||
// Step 4: Compute the "height" of the graph by looking at the node with the highest y value
|
||||
graphData.height = 1
|
||||
graphData.height = 1;
|
||||
for (const node of [...Object.values(graphData.nodes_by_id)]) {
|
||||
graphData.height = Math.max(graphData.height, (node.y ?? 0) + 1)
|
||||
graphData.height = Math.max(graphData.height, (node.y ?? 0) + 1);
|
||||
}
|
||||
|
||||
return graphData
|
||||
})
|
||||
return graphData;
|
||||
});
|
||||
|
||||
const contentWidth = computed(() => {
|
||||
return graph.value.columns * (boxWidth + marginX) + 2 * paddingX
|
||||
})
|
||||
return graph.value.columns * (boxWidth + marginX) + 2 * paddingX;
|
||||
});
|
||||
|
||||
const contentHeight = computed(() => {
|
||||
return graph.value.height * (boxHeight + marginY)
|
||||
})
|
||||
return graph.value.height * (boxHeight + marginY);
|
||||
});
|
||||
|
||||
const viewBox = computed(() => {
|
||||
return `0 0 ${contentWidth.value} ${contentHeight.value}`
|
||||
})
|
||||
return `0 0 ${contentWidth.value} ${contentHeight.value}`;
|
||||
});
|
||||
|
||||
function createZoom () {
|
||||
if (!svg.value) return
|
||||
function createZoom() {
|
||||
if (!svg.value) return;
|
||||
|
||||
const viewportHeight = svg.value.clientHeight
|
||||
const viewportWidth = svg.value.clientWidth
|
||||
defaultScale.value = 1
|
||||
const viewportHeight = svg.value.clientHeight;
|
||||
const viewportWidth = svg.value.clientWidth;
|
||||
defaultScale.value = 1;
|
||||
|
||||
zoom.value = d3
|
||||
.zoom()
|
||||
.scaleExtent([Math.min(defaultScale.value * 0.5, 1), Math.max(5, contentHeight.value / viewportHeight, contentWidth.value / viewportWidth)])
|
||||
.extent([[0, 0], [viewportWidth, viewportHeight]])
|
||||
.scaleExtent([
|
||||
Math.min(defaultScale.value * 0.5, 1),
|
||||
Math.max(
|
||||
5,
|
||||
contentHeight.value / viewportHeight,
|
||||
contentWidth.value / viewportWidth,
|
||||
),
|
||||
])
|
||||
.extent([
|
||||
[0, 0],
|
||||
[viewportWidth, viewportHeight],
|
||||
])
|
||||
.filter((event: any) => {
|
||||
const wheeled = event.type === 'wheel'
|
||||
const mouseDrag
|
||||
= event.type === 'mousedown'
|
||||
|| event.type === 'mouseup'
|
||||
|| event.type === 'mousemove'
|
||||
const touch
|
||||
= event.type === 'touchstart'
|
||||
|| event.type === 'touchmove'
|
||||
|| event.type === 'touchstop'
|
||||
return (wheeled || mouseDrag || touch) && maximized.value
|
||||
const wheeled = event.type === "wheel";
|
||||
const mouseDrag =
|
||||
event.type === "mousedown" ||
|
||||
event.type === "mouseup" ||
|
||||
event.type === "mousemove";
|
||||
const touch =
|
||||
event.type === "touchstart" ||
|
||||
event.type === "touchmove" ||
|
||||
event.type === "touchstop";
|
||||
return (wheeled || mouseDrag || touch) && maximized.value;
|
||||
})
|
||||
.wheelDelta((event: any) => {
|
||||
// In contrast to default implementation, do not use a factor 10 if ctrl is pressed
|
||||
return -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002)
|
||||
})
|
||||
.on('zoom', (event: any) => {
|
||||
zoomTransform.value = event.transform
|
||||
return (
|
||||
-event.deltaY *
|
||||
(event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002)
|
||||
);
|
||||
})
|
||||
.on("zoom", (event: any) => {
|
||||
zoomTransform.value = event.transform;
|
||||
});
|
||||
|
||||
const initTransform = d3.zoomIdentity
|
||||
.scale(defaultScale.value)
|
||||
.translate(0, 0)
|
||||
zoomTransform.value = initTransform
|
||||
.translate(0, 0);
|
||||
zoomTransform.value = initTransform;
|
||||
|
||||
// This sets correct d3 internal state for the initial centering
|
||||
d3.select(svg.value)
|
||||
.call(zoom.value.transform, initTransform)
|
||||
d3.select(svg.value).call(zoom.value.transform, initTransform);
|
||||
|
||||
const svgSelection = d3.select(svg.value).call(zoom.value)
|
||||
svgSelection.on('touchmove.zoom', null)
|
||||
const svgSelection = d3.select(svg.value).call(zoom.value);
|
||||
svgSelection.on("touchmove.zoom", null);
|
||||
// TODO touch support
|
||||
}
|
||||
|
||||
watch(maximized, () => {
|
||||
nextTick(() => {
|
||||
createZoom()
|
||||
})
|
||||
})
|
||||
createZoom();
|
||||
});
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
createZoom()
|
||||
window.addEventListener('resize', createZoom)
|
||||
})
|
||||
createZoom();
|
||||
window.addEventListener("resize", createZoom);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', createZoom)
|
||||
})
|
||||
|
||||
window.removeEventListener("resize", createZoom);
|
||||
});
|
||||
</script>
|
||||
<template lang="pug">
|
||||
div(:class="'checkin-rules-visualization ' + (maximized ? 'maximized' : '')")
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
/* global gettext, pgettext */
|
||||
|
||||
export const TEXTS = {
|
||||
and: gettext('All of the conditions below (AND)'),
|
||||
or: gettext('At least one of the conditions below (OR)'),
|
||||
date_from: gettext('Event start'),
|
||||
date_to: gettext('Event end'),
|
||||
date_admission: gettext('Event admission'),
|
||||
date_custom: gettext('custom date and time'),
|
||||
date_customtime: gettext('custom time'),
|
||||
date_tolerance: gettext('Tolerance (minutes)'),
|
||||
condition_add: gettext('Add condition'),
|
||||
minutes: gettext('minutes'),
|
||||
duplicate: gettext('Duplicate'),
|
||||
status_present: pgettext('entry_status', 'present'),
|
||||
status_absent: pgettext('entry_status', 'absent'),
|
||||
}
|
||||
and: gettext("All of the conditions below (AND)"),
|
||||
or: gettext("At least one of the conditions below (OR)"),
|
||||
date_from: gettext("Event start"),
|
||||
date_to: gettext("Event end"),
|
||||
date_admission: gettext("Event admission"),
|
||||
date_custom: gettext("custom date and time"),
|
||||
date_customtime: gettext("custom time"),
|
||||
date_tolerance: gettext("Tolerance (minutes)"),
|
||||
condition_add: gettext("Add condition"),
|
||||
minutes: gettext("minutes"),
|
||||
duplicate: gettext("Duplicate"),
|
||||
status_present: pgettext("entry_status", "present"),
|
||||
status_absent: pgettext("entry_status", "absent"),
|
||||
};
|
||||
|
||||
export const TYPEOPS = {
|
||||
// Every change to our supported JSON logic must be done
|
||||
@@ -26,173 +26,173 @@ export const TYPEOPS = {
|
||||
// * in pretixscan-ios
|
||||
product: {
|
||||
inList: {
|
||||
label: gettext('is one of'),
|
||||
label: gettext("is one of"),
|
||||
cardinality: 2,
|
||||
}
|
||||
},
|
||||
},
|
||||
variation: {
|
||||
inList: {
|
||||
label: gettext('is one of'),
|
||||
label: gettext("is one of"),
|
||||
cardinality: 2,
|
||||
}
|
||||
},
|
||||
},
|
||||
gate: {
|
||||
inList: {
|
||||
label: gettext('is one of'),
|
||||
label: gettext("is one of"),
|
||||
cardinality: 2,
|
||||
}
|
||||
},
|
||||
},
|
||||
datetime: {
|
||||
isBefore: {
|
||||
label: gettext('is before'),
|
||||
label: gettext("is before"),
|
||||
cardinality: 2,
|
||||
},
|
||||
isAfter: {
|
||||
label: gettext('is after'),
|
||||
label: gettext("is after"),
|
||||
cardinality: 2,
|
||||
},
|
||||
},
|
||||
enum_entry_status: {
|
||||
'==': {
|
||||
label: gettext('='),
|
||||
"==": {
|
||||
label: gettext("="),
|
||||
cardinality: 2,
|
||||
},
|
||||
},
|
||||
int_by_datetime: {
|
||||
'<': {
|
||||
label: '<',
|
||||
"<": {
|
||||
label: "<",
|
||||
cardinality: 2,
|
||||
},
|
||||
'<=': {
|
||||
label: '≤',
|
||||
"<=": {
|
||||
label: "≤",
|
||||
cardinality: 2,
|
||||
},
|
||||
'>': {
|
||||
label: '>',
|
||||
">": {
|
||||
label: ">",
|
||||
cardinality: 2,
|
||||
},
|
||||
'>=': {
|
||||
label: '≥',
|
||||
">=": {
|
||||
label: "≥",
|
||||
cardinality: 2,
|
||||
},
|
||||
'==': {
|
||||
label: '=',
|
||||
"==": {
|
||||
label: "=",
|
||||
cardinality: 2,
|
||||
},
|
||||
'!=': {
|
||||
label: '≠',
|
||||
"!=": {
|
||||
label: "≠",
|
||||
cardinality: 2,
|
||||
},
|
||||
},
|
||||
int: {
|
||||
'<': {
|
||||
label: '<',
|
||||
"<": {
|
||||
label: "<",
|
||||
cardinality: 2,
|
||||
},
|
||||
'<=': {
|
||||
label: '≤',
|
||||
"<=": {
|
||||
label: "≤",
|
||||
cardinality: 2,
|
||||
},
|
||||
'>': {
|
||||
label: '>',
|
||||
">": {
|
||||
label: ">",
|
||||
cardinality: 2,
|
||||
},
|
||||
'>=': {
|
||||
label: '≥',
|
||||
">=": {
|
||||
label: "≥",
|
||||
cardinality: 2,
|
||||
},
|
||||
'==': {
|
||||
label: '=',
|
||||
"==": {
|
||||
label: "=",
|
||||
cardinality: 2,
|
||||
},
|
||||
'!=': {
|
||||
label: '≠',
|
||||
"!=": {
|
||||
label: "≠",
|
||||
cardinality: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export const VARS = {
|
||||
product: {
|
||||
label: gettext('Product'),
|
||||
type: 'product',
|
||||
label: gettext("Product"),
|
||||
type: "product",
|
||||
},
|
||||
variation: {
|
||||
label: gettext('Product variation'),
|
||||
type: 'variation',
|
||||
label: gettext("Product variation"),
|
||||
type: "variation",
|
||||
},
|
||||
gate: {
|
||||
label: gettext('Gate'),
|
||||
type: 'gate',
|
||||
label: gettext("Gate"),
|
||||
type: "gate",
|
||||
},
|
||||
now: {
|
||||
label: gettext('Current date and time'),
|
||||
type: 'datetime',
|
||||
label: gettext("Current date and time"),
|
||||
type: "datetime",
|
||||
},
|
||||
now_isoweekday: {
|
||||
label: gettext('Current day of the week (1 = Monday, 7 = Sunday)'),
|
||||
type: 'int',
|
||||
label: gettext("Current day of the week (1 = Monday, 7 = Sunday)"),
|
||||
type: "int",
|
||||
},
|
||||
entry_status: {
|
||||
label: gettext('Current entry status'),
|
||||
type: 'enum_entry_status',
|
||||
label: gettext("Current entry status"),
|
||||
type: "enum_entry_status",
|
||||
},
|
||||
entries_number: {
|
||||
label: gettext('Number of previous entries'),
|
||||
type: 'int',
|
||||
label: gettext("Number of previous entries"),
|
||||
type: "int",
|
||||
},
|
||||
entries_today: {
|
||||
label: gettext('Number of previous entries since midnight'),
|
||||
type: 'int',
|
||||
label: gettext("Number of previous entries since midnight"),
|
||||
type: "int",
|
||||
},
|
||||
entries_since: {
|
||||
label: gettext('Number of previous entries since'),
|
||||
type: 'int_by_datetime',
|
||||
label: gettext("Number of previous entries since"),
|
||||
type: "int_by_datetime",
|
||||
},
|
||||
entries_before: {
|
||||
label: gettext('Number of previous entries before'),
|
||||
type: 'int_by_datetime',
|
||||
label: gettext("Number of previous entries before"),
|
||||
type: "int_by_datetime",
|
||||
},
|
||||
entries_days: {
|
||||
label: gettext('Number of days with a previous entry'),
|
||||
type: 'int',
|
||||
label: gettext("Number of days with a previous entry"),
|
||||
type: "int",
|
||||
},
|
||||
entries_days_since: {
|
||||
label: gettext('Number of days with a previous entry since'),
|
||||
type: 'int_by_datetime',
|
||||
label: gettext("Number of days with a previous entry since"),
|
||||
type: "int_by_datetime",
|
||||
},
|
||||
entries_days_before: {
|
||||
label: gettext('Number of days with a previous entry before'),
|
||||
type: 'int_by_datetime',
|
||||
label: gettext("Number of days with a previous entry before"),
|
||||
type: "int_by_datetime",
|
||||
},
|
||||
minutes_since_last_entry: {
|
||||
label: gettext('Minutes since last entry (-1 on first entry)'),
|
||||
type: 'int',
|
||||
label: gettext("Minutes since last entry (-1 on first entry)"),
|
||||
type: "int",
|
||||
},
|
||||
minutes_since_first_entry: {
|
||||
label: gettext('Minutes since first entry (-1 on first entry)'),
|
||||
type: 'int',
|
||||
label: gettext("Minutes since first entry (-1 on first entry)"),
|
||||
type: "int",
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export const DATETIME_OPTIONS = {
|
||||
format: document.body.dataset.datetimeformat,
|
||||
locale: document.body.dataset.datetimelocale,
|
||||
useCurrent: false,
|
||||
icons: {
|
||||
time: 'fa fa-clock-o',
|
||||
date: 'fa fa-calendar',
|
||||
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'
|
||||
}
|
||||
}
|
||||
time: "fa fa-clock-o",
|
||||
date: "fa fa-calendar",
|
||||
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",
|
||||
},
|
||||
};
|
||||
|
||||
export const TIME_OPTIONS = {
|
||||
...DATETIME_OPTIONS,
|
||||
format: document.body.dataset.timeformat,
|
||||
}
|
||||
};
|
||||
@@ -1,21 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { DATETIME_OPTIONS } from './constants'
|
||||
import { ref, watch, onMounted, onUnmounted } from "vue";
|
||||
import { DATETIME_OPTIONS } from "./constants";
|
||||
|
||||
const props = defineProps<{
|
||||
required?: boolean
|
||||
value?: string
|
||||
}>()
|
||||
required?: boolean;
|
||||
value?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
input: [value: string]
|
||||
}>()
|
||||
input: [value: string];
|
||||
}>();
|
||||
|
||||
const input = ref<HTMLInputElement | null>(null)
|
||||
const input = ref<HTMLInputElement | null>(null);
|
||||
|
||||
watch(() => props.value, (val) => {
|
||||
$(input.value).data('DateTimePicker').date(moment(val))
|
||||
})
|
||||
watch(
|
||||
() => props.value,
|
||||
(val) => {
|
||||
$(input.value).data("DateTimePicker").date(moment(val));
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
$(input.value)
|
||||
@@ -23,22 +26,22 @@ onMounted(() => {
|
||||
...DATETIME_OPTIONS,
|
||||
showClear: props.required,
|
||||
})
|
||||
.trigger('change')
|
||||
.on('dp.change', function (this: HTMLElement) {
|
||||
emit('input', $(this).data('DateTimePicker').date().toISOString())
|
||||
})
|
||||
.trigger("change")
|
||||
.on("dp.change", function (this: HTMLElement) {
|
||||
emit("input", $(this).data("DateTimePicker").date().toISOString());
|
||||
});
|
||||
if (!props.value) {
|
||||
$(input.value).data('DateTimePicker').viewDate(moment().hour(0).minute(0).second(0).millisecond(0))
|
||||
$(input.value)
|
||||
.data("DateTimePicker")
|
||||
.viewDate(moment().hour(0).minute(0).second(0).millisecond(0));
|
||||
} else {
|
||||
$(input.value).data('DateTimePicker').date(moment(props.value))
|
||||
$(input.value).data("DateTimePicker").date(moment(props.value));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
$(input.value)
|
||||
.off()
|
||||
.datetimepicker('destroy')
|
||||
})
|
||||
$(input.value).off().datetimepicker("destroy");
|
||||
});
|
||||
</script>
|
||||
<template lang="pug">
|
||||
input.form-control(ref="input")
|
||||
|
||||
@@ -1,70 +1,95 @@
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
export const allProducts = ref(false)
|
||||
export const limitProducts = ref<number[]>([])
|
||||
export const allProducts = ref(false);
|
||||
export const limitProducts = ref<number[]>([]);
|
||||
|
||||
function updateProducts () {
|
||||
allProducts.value = document.querySelector<HTMLInputElement>('#id_all_products')?.checked ?? false
|
||||
limitProducts.value = Array.from(document.querySelectorAll<HTMLInputElement>('input[name=limit_products]:checked')).map(el => parseInt(el.value))
|
||||
function updateProducts() {
|
||||
allProducts.value =
|
||||
document.querySelector<HTMLInputElement>("#id_all_products")?.checked ??
|
||||
false;
|
||||
limitProducts.value = Array.from(
|
||||
document.querySelectorAll<HTMLInputElement>(
|
||||
"input[name=limit_products]:checked",
|
||||
),
|
||||
).map((el) => parseInt(el.value));
|
||||
}
|
||||
|
||||
// listen to change events for products
|
||||
document.querySelectorAll('#id_all_products, input[name=limit_products]').forEach(el => el.addEventListener('change', updateProducts))
|
||||
updateProducts()
|
||||
document
|
||||
.querySelectorAll("#id_all_products, input[name=limit_products]")
|
||||
.forEach((el) => el.addEventListener("change", updateProducts));
|
||||
updateProducts();
|
||||
|
||||
export const rules = ref<any>({})
|
||||
export const rules = ref<any>({});
|
||||
|
||||
// grab rules from hidden input
|
||||
const rulesInput = document.querySelector<HTMLInputElement>('#id_rules')
|
||||
const rulesInput = document.querySelector<HTMLInputElement>("#id_rules");
|
||||
if (rulesInput?.value) {
|
||||
rules.value = JSON.parse(rulesInput.value) ?? {}
|
||||
rules.value = JSON.parse(rulesInput.value) ?? {};
|
||||
}
|
||||
|
||||
// sync back to hidden input
|
||||
watch(rules, (newVal) => {
|
||||
if (!rulesInput) return
|
||||
rulesInput.value = JSON.stringify(newVal)
|
||||
}, { deep: true })
|
||||
watch(
|
||||
rules,
|
||||
(newVal) => {
|
||||
if (!rulesInput) return;
|
||||
rulesInput.value = JSON.stringify(newVal);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
export const activeItems = ref<any[]>([])
|
||||
export const allItems = ref<any[]>([])
|
||||
export const activeItems = ref<any[]>([]);
|
||||
export const allItems = ref<any[]>([]);
|
||||
|
||||
const itemsEl = document.querySelector('#items')
|
||||
const itemsEl = document.querySelector("#items");
|
||||
if (itemsEl?.textContent) {
|
||||
allItems.value = JSON.parse(itemsEl.textContent || '[]')
|
||||
activeItems.value = allItems.value.filter(item => item.active)
|
||||
allItems.value = JSON.parse(itemsEl.textContent || "[]");
|
||||
activeItems.value = allItems.value.filter((item) => item.active);
|
||||
|
||||
function checkForInvalidIds (validProducts: Record<string, string>, validVariations: Record<string, string>, rule: any) {
|
||||
if (rule['and']) {
|
||||
for (const child of rule['and'])
|
||||
checkForInvalidIds(validProducts, validVariations, child)
|
||||
} else if (rule['or']) {
|
||||
for (const child of rule['or'])
|
||||
checkForInvalidIds(validProducts, validVariations, child)
|
||||
} else if (rule['inList'] && rule['inList'][0]['var'] === 'product') {
|
||||
for (const item of rule['inList'][1]['objectList']) {
|
||||
if (!validProducts[item['lookup'][1]])
|
||||
item['lookup'][2] = '[' + gettext('Error: Product not found!') + ']'
|
||||
else
|
||||
item['lookup'][2] = validProducts[item['lookup'][1]]
|
||||
function checkForInvalidIds(
|
||||
validProducts: Record<string, string>,
|
||||
validVariations: Record<string, string>,
|
||||
rule: any,
|
||||
) {
|
||||
if (rule["and"]) {
|
||||
for (const child of rule["and"])
|
||||
checkForInvalidIds(validProducts, validVariations, child);
|
||||
} else if (rule["or"]) {
|
||||
for (const child of rule["or"])
|
||||
checkForInvalidIds(validProducts, validVariations, child);
|
||||
} else if (rule["inList"] && rule["inList"][0]["var"] === "product") {
|
||||
for (const item of rule["inList"][1]["objectList"]) {
|
||||
if (!validProducts[item["lookup"][1]])
|
||||
item["lookup"][2] = "[" + gettext("Error: Product not found!") + "]";
|
||||
else item["lookup"][2] = validProducts[item["lookup"][1]];
|
||||
}
|
||||
} else if (rule['inList'] && rule['inList'][0]['var'] === 'variation') {
|
||||
for (const item of rule['inList'][1]['objectList']) {
|
||||
if (!validVariations[item['lookup'][1]])
|
||||
item['lookup'][2] = '[' + gettext('Error: Variation not found!') + ']'
|
||||
else
|
||||
item['lookup'][2] = validVariations[item['lookup'][1]]
|
||||
} else if (rule["inList"] && rule["inList"][0]["var"] === "variation") {
|
||||
for (const item of rule["inList"][1]["objectList"]) {
|
||||
if (!validVariations[item["lookup"][1]])
|
||||
item["lookup"][2] =
|
||||
"[" + gettext("Error: Variation not found!") + "]";
|
||||
else item["lookup"][2] = validVariations[item["lookup"][1]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkForInvalidIds(
|
||||
Object.fromEntries(allItems.value.map(p => [p.id, p.name])),
|
||||
Object.fromEntries(allItems.value.flatMap(p => p.variations?.map(v => [v.id, p.name + ' – ' + v.name]) ?? [])),
|
||||
rules.value
|
||||
)
|
||||
Object.fromEntries(allItems.value.map((p) => [p.id, p.name])),
|
||||
Object.fromEntries(
|
||||
allItems.value.flatMap(
|
||||
(p) => p.variations?.map((v) => [v.id, p.name + " – " + v.name]) ?? [],
|
||||
),
|
||||
),
|
||||
rules.value,
|
||||
);
|
||||
}
|
||||
|
||||
export const productSelectURL = ref(document.querySelector('#product-select2')?.textContent)
|
||||
export const variationSelectURL = ref(document.querySelector('#variations-select2')?.textContent)
|
||||
export const gateSelectURL = ref(document.querySelector('#gates-select2')?.textContent)
|
||||
export const productSelectURL = ref(
|
||||
document.querySelector("#product-select2")?.textContent,
|
||||
);
|
||||
export const variationSelectURL = ref(
|
||||
document.querySelector("#variations-select2")?.textContent,
|
||||
);
|
||||
export const gateSelectURL = ref(
|
||||
document.querySelector("#gates-select2")?.textContent,
|
||||
);
|
||||
@@ -1,12 +1,12 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
|
||||
const app = createApp(App)
|
||||
app.mount('#rules-editor')
|
||||
const app = createApp(App);
|
||||
app.mount("#rules-editor");
|
||||
|
||||
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)
|
||||
}
|
||||
console.error("[VUE]", info, error);
|
||||
};
|
||||
@@ -1,93 +1,110 @@
|
||||
export function convertToDNF (rules) {
|
||||
export function convertToDNF(rules) {
|
||||
// Converts a set of rules to disjunctive normal form, i.e. returns something of the form
|
||||
// `(a AND b AND c) OR (a AND d AND f)`
|
||||
// without further nesting.
|
||||
if (typeof rules !== 'object' || Array.isArray(rules) || rules === null) {
|
||||
return rules
|
||||
if (typeof rules !== "object" || Array.isArray(rules) || rules === null) {
|
||||
return rules;
|
||||
}
|
||||
|
||||
function _distribute_or_over_and (r) {
|
||||
let operator = Object.keys(r)[0]
|
||||
let values = r[operator]
|
||||
if (operator === 'and') {
|
||||
let arg_to_distribute = null
|
||||
let other_args = []
|
||||
function _distribute_or_over_and(r) {
|
||||
let operator = Object.keys(r)[0];
|
||||
let values = r[operator];
|
||||
if (operator === "and") {
|
||||
let arg_to_distribute = null;
|
||||
let other_args = [];
|
||||
for (let arg of values) {
|
||||
if (typeof arg === 'object' && !Array.isArray(arg) && typeof arg['or'] !== 'undefined' && arg_to_distribute === null) {
|
||||
arg_to_distribute = arg
|
||||
if (
|
||||
typeof arg === "object" &&
|
||||
!Array.isArray(arg) &&
|
||||
typeof arg["or"] !== "undefined" &&
|
||||
arg_to_distribute === null
|
||||
) {
|
||||
arg_to_distribute = arg;
|
||||
} else {
|
||||
other_args.push(arg)
|
||||
other_args.push(arg);
|
||||
}
|
||||
}
|
||||
if (arg_to_distribute === null) {
|
||||
return r
|
||||
return r;
|
||||
}
|
||||
let or_operands = []
|
||||
for (let dval of arg_to_distribute['or']) {
|
||||
or_operands.push({ and: other_args.concat([dval]) })
|
||||
let or_operands = [];
|
||||
for (let dval of arg_to_distribute["or"]) {
|
||||
or_operands.push({ and: other_args.concat([dval]) });
|
||||
}
|
||||
return {
|
||||
or: or_operands
|
||||
}
|
||||
or: or_operands,
|
||||
};
|
||||
} else if (!operator) {
|
||||
return r
|
||||
} else if (operator === '!' || operator === '!!' || operator === '?:' || operator === 'if') {
|
||||
console.warn('Operator ' + operator + ' currently unsupported by convert_to_dnf')
|
||||
return r
|
||||
return r;
|
||||
} else if (
|
||||
operator === "!" ||
|
||||
operator === "!!" ||
|
||||
operator === "?:" ||
|
||||
operator === "if"
|
||||
) {
|
||||
console.warn(
|
||||
"Operator " + operator + " currently unsupported by convert_to_dnf",
|
||||
);
|
||||
return r;
|
||||
} else {
|
||||
return r
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
function _simplify_chained_operators (r) {
|
||||
function _simplify_chained_operators(r) {
|
||||
// Simplify `(a OR b) OR (c or d)` to `a OR b OR c OR d` and the same with `AND`
|
||||
if (typeof r !== 'object' || Array.isArray(r)) {
|
||||
return r
|
||||
if (typeof r !== "object" || Array.isArray(r)) {
|
||||
return r;
|
||||
}
|
||||
let operator = Object.keys(r)[0]
|
||||
let values = r[operator]
|
||||
if (operator !== 'or' && operator !== 'and') {
|
||||
return r
|
||||
let operator = Object.keys(r)[0];
|
||||
let values = r[operator];
|
||||
if (operator !== "or" && operator !== "and") {
|
||||
return r;
|
||||
}
|
||||
let new_values = []
|
||||
let new_values = [];
|
||||
for (let v of values) {
|
||||
if (typeof v !== 'object' || Array.isArray(v) || typeof v[operator] === 'undefined') {
|
||||
new_values.push(v)
|
||||
if (
|
||||
typeof v !== "object" ||
|
||||
Array.isArray(v) ||
|
||||
typeof v[operator] === "undefined"
|
||||
) {
|
||||
new_values.push(v);
|
||||
} else {
|
||||
new_values.push(...v[operator])
|
||||
new_values.push(...v[operator]);
|
||||
}
|
||||
}
|
||||
let result = {}
|
||||
result[operator] = new_values
|
||||
return result
|
||||
let result = {};
|
||||
result[operator] = new_values;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Run _distribute_or_over_and on until it no longer changes anything. Do so recursively
|
||||
// for the full expression tree.
|
||||
let old_rules = rules
|
||||
let old_rules = rules;
|
||||
while (true) {
|
||||
rules = _distribute_or_over_and(rules)
|
||||
let operator = Object.keys(rules)[0]
|
||||
let values = rules[operator]
|
||||
let no_list = false
|
||||
rules = _distribute_or_over_and(rules);
|
||||
let operator = Object.keys(rules)[0];
|
||||
let values = rules[operator];
|
||||
let no_list = false;
|
||||
if (!Array.isArray(values)) {
|
||||
values = [values]
|
||||
no_list = true
|
||||
values = [values];
|
||||
no_list = true;
|
||||
}
|
||||
rules = {}
|
||||
rules = {};
|
||||
if (!no_list) {
|
||||
rules[operator] = []
|
||||
rules[operator] = [];
|
||||
for (let v of values) {
|
||||
rules[operator].push(convertToDNF(v))
|
||||
rules[operator].push(convertToDNF(v));
|
||||
}
|
||||
} else {
|
||||
rules[operator] = convertToDNF(values[0])
|
||||
rules[operator] = convertToDNF(values[0]);
|
||||
}
|
||||
if (JSON.stringify(old_rules) === JSON.stringify(rules)) { // Let's hope this is good enough...
|
||||
break
|
||||
if (JSON.stringify(old_rules) === JSON.stringify(rules)) {
|
||||
// Let's hope this is good enough...
|
||||
break;
|
||||
}
|
||||
old_rules = rules
|
||||
old_rules = rules;
|
||||
}
|
||||
rules = _simplify_chained_operators(rules)
|
||||
return rules
|
||||
rules = _simplify_chained_operators(rules);
|
||||
return rules;
|
||||
}
|
||||
@@ -1,119 +1,141 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, watch, onMounted, onUnmounted } from "vue";
|
||||
|
||||
declare const $: any
|
||||
declare const $: any;
|
||||
|
||||
export interface ObjectListItem {
|
||||
lookup: [string, number | string, string]
|
||||
lookup: [string, number | string, string];
|
||||
}
|
||||
|
||||
export interface ObjectList {
|
||||
objectList: ObjectListItem[]
|
||||
objectList: ObjectListItem[];
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
required?: boolean
|
||||
value?: ObjectList
|
||||
placeholder?: string
|
||||
url?: string
|
||||
multiple?: boolean
|
||||
}>()
|
||||
required?: boolean;
|
||||
value?: ObjectList;
|
||||
placeholder?: string;
|
||||
url?: string;
|
||||
multiple?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
input: [value: any[]]
|
||||
}>()
|
||||
input: [value: any[]];
|
||||
}>();
|
||||
|
||||
const select = ref<HTMLSelectElement | null>(null)
|
||||
const select = ref<HTMLSelectElement | null>(null);
|
||||
|
||||
function opts () {
|
||||
function opts() {
|
||||
return {
|
||||
theme: 'bootstrap',
|
||||
theme: "bootstrap",
|
||||
delay: 100,
|
||||
width: '100%',
|
||||
width: "100%",
|
||||
multiple: true,
|
||||
allowClear: props.required,
|
||||
language: $('body').attr('data-select2-locale'),
|
||||
language: $("body").attr("data-select2-locale"),
|
||||
ajax: {
|
||||
url: props.url,
|
||||
data: function (params: { term: string; page?: number }) {
|
||||
return {
|
||||
query: params.term,
|
||||
page: params.page || 1
|
||||
}
|
||||
}
|
||||
page: params.page || 1,
|
||||
};
|
||||
},
|
||||
},
|
||||
templateResult: function (res: { id?: string; text: string }) {
|
||||
if (!res.id) {
|
||||
return res.text
|
||||
return res.text;
|
||||
}
|
||||
const $ret = $('<span>').append(
|
||||
$('<span>').addClass('primary').append($('<div>').text(res.text).html())
|
||||
)
|
||||
return $ret
|
||||
const $ret = $("<span>").append(
|
||||
$("<span>")
|
||||
.addClass("primary")
|
||||
.append($("<div>").text(res.text).html()),
|
||||
);
|
||||
return $ret;
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function build () {
|
||||
function build() {
|
||||
$(select.value)
|
||||
.empty()
|
||||
.select2(opts())
|
||||
.val(props.value || '')
|
||||
.trigger('change')
|
||||
.on('change', function (this: HTMLElement) {
|
||||
emit('input', $(this).select2('data'))
|
||||
})
|
||||
.val(props.value || "")
|
||||
.trigger("change")
|
||||
.on("change", function (this: HTMLElement) {
|
||||
emit("input", $(this).select2("data"));
|
||||
});
|
||||
if (props.value) {
|
||||
for (let i = 0; i < props.value.objectList.length; i++) {
|
||||
const option = new Option(props.value.objectList[i].lookup[2], String(props.value.objectList[i].lookup[1]), true, true)
|
||||
$(select.value).append(option)
|
||||
const option = new Option(
|
||||
props.value.objectList[i].lookup[2],
|
||||
String(props.value.objectList[i].lookup[1]),
|
||||
true,
|
||||
true,
|
||||
);
|
||||
$(select.value).append(option);
|
||||
}
|
||||
}
|
||||
$(select.value).trigger('change')
|
||||
$(select.value).trigger("change");
|
||||
}
|
||||
|
||||
watch(() => props.placeholder, () => {
|
||||
$(select.value).select2('destroy')
|
||||
build()
|
||||
})
|
||||
watch(
|
||||
() => props.placeholder,
|
||||
() => {
|
||||
$(select.value).select2("destroy");
|
||||
build();
|
||||
},
|
||||
);
|
||||
|
||||
watch(() => props.required, () => {
|
||||
$(select.value).select2('destroy')
|
||||
build()
|
||||
})
|
||||
watch(
|
||||
() => props.required,
|
||||
() => {
|
||||
$(select.value).select2("destroy");
|
||||
build();
|
||||
},
|
||||
);
|
||||
|
||||
watch(() => props.url, () => {
|
||||
$(select.value).select2('destroy')
|
||||
build()
|
||||
})
|
||||
watch(
|
||||
() => props.url,
|
||||
() => {
|
||||
$(select.value).select2("destroy");
|
||||
build();
|
||||
},
|
||||
);
|
||||
|
||||
watch(() => props.value, (newval, oldval) => {
|
||||
if (JSON.stringify(newval) !== JSON.stringify(oldval)) {
|
||||
$(select.value).empty()
|
||||
if (newval) {
|
||||
for (let i = 0; i < newval.objectList.length; i++) {
|
||||
const option = new Option(newval.objectList[i].lookup[2], String(newval.objectList[i].lookup[1]), true, true)
|
||||
$(select.value).append(option)
|
||||
watch(
|
||||
() => props.value,
|
||||
(newval, oldval) => {
|
||||
if (JSON.stringify(newval) !== JSON.stringify(oldval)) {
|
||||
$(select.value).empty();
|
||||
if (newval) {
|
||||
for (let i = 0; i < newval.objectList.length; i++) {
|
||||
const option = new Option(
|
||||
newval.objectList[i].lookup[2],
|
||||
String(newval.objectList[i].lookup[1]),
|
||||
true,
|
||||
true,
|
||||
);
|
||||
$(select.value).append(option);
|
||||
}
|
||||
}
|
||||
$(select.value).trigger("change");
|
||||
}
|
||||
$(select.value).trigger('change')
|
||||
}
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
let rawSelectEl: HTMLSelectElement | null = null
|
||||
let rawSelectEl: HTMLSelectElement | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
rawSelectEl = select.value
|
||||
build()
|
||||
})
|
||||
rawSelectEl = select.value;
|
||||
build();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (!rawSelectEl) return
|
||||
$(rawSelectEl)
|
||||
.off()
|
||||
.select2('destroy')
|
||||
rawSelectEl = null
|
||||
})
|
||||
if (!rawSelectEl) return;
|
||||
$(rawSelectEl).off().select2("destroy");
|
||||
rawSelectEl = null;
|
||||
});
|
||||
</script>
|
||||
<template lang="pug">
|
||||
select(ref="select")
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { TIME_OPTIONS } from './constants'
|
||||
import { ref, watch, onMounted, onUnmounted } from "vue";
|
||||
import { TIME_OPTIONS } from "./constants";
|
||||
|
||||
const props = defineProps<{
|
||||
required?: boolean
|
||||
value?: string
|
||||
}>()
|
||||
required?: boolean;
|
||||
value?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
input: [value: string]
|
||||
}>()
|
||||
input: [value: string];
|
||||
}>();
|
||||
|
||||
const input = ref<HTMLInputElement | null>(null)
|
||||
const input = ref<HTMLInputElement | null>(null);
|
||||
|
||||
watch(() => props.value, (val) => {
|
||||
$(input.value).data('DateTimePicker').date(val)
|
||||
})
|
||||
watch(
|
||||
() => props.value,
|
||||
(val) => {
|
||||
$(input.value).data("DateTimePicker").date(val);
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
$(input.value)
|
||||
@@ -23,22 +26,22 @@ onMounted(() => {
|
||||
...TIME_OPTIONS,
|
||||
showClear: props.required,
|
||||
})
|
||||
.trigger('change')
|
||||
.on('dp.change', function (this: HTMLElement) {
|
||||
emit('input', $(this).data('DateTimePicker').date().format('HH:mm:ss'))
|
||||
})
|
||||
.trigger("change")
|
||||
.on("dp.change", function (this: HTMLElement) {
|
||||
emit("input", $(this).data("DateTimePicker").date().format("HH:mm:ss"));
|
||||
});
|
||||
if (!props.value) {
|
||||
$(input.value).data('DateTimePicker').viewDate(moment().hour(0).minute(0).second(0).millisecond(0))
|
||||
$(input.value)
|
||||
.data("DateTimePicker")
|
||||
.viewDate(moment().hour(0).minute(0).second(0).millisecond(0));
|
||||
} else {
|
||||
$(input.value).data('DateTimePicker').date(props.value)
|
||||
$(input.value).data("DateTimePicker").date(props.value);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
$(input.value)
|
||||
.off()
|
||||
.datetimepicker('destroy')
|
||||
})
|
||||
$(input.value).off().datetimepicker("destroy");
|
||||
});
|
||||
</script>
|
||||
<template lang="pug">
|
||||
input.form-control(ref="input")
|
||||
|
||||
@@ -1,43 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { TEXTS, VARS, TYPEOPS } from './constants'
|
||||
import { computed } from "vue";
|
||||
import { TEXTS, VARS, TYPEOPS } from "./constants";
|
||||
|
||||
declare const $: any
|
||||
declare const moment: any
|
||||
declare const $: any;
|
||||
declare const moment: any;
|
||||
|
||||
interface GraphNode {
|
||||
rule: any
|
||||
column: number
|
||||
children: string[]
|
||||
y: number
|
||||
parent?: GraphNode
|
||||
rule: any;
|
||||
column: number;
|
||||
children: string[];
|
||||
y: number;
|
||||
parent?: GraphNode;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
node: GraphNode
|
||||
nodeid: string
|
||||
children: GraphNode[]
|
||||
boxWidth: number
|
||||
boxHeight: number
|
||||
marginX: number
|
||||
marginY: number
|
||||
paddingX: number
|
||||
}>()
|
||||
node: GraphNode;
|
||||
nodeid: string;
|
||||
children: GraphNode[];
|
||||
boxWidth: number;
|
||||
boxHeight: number;
|
||||
marginX: number;
|
||||
marginY: number;
|
||||
paddingX: number;
|
||||
}>();
|
||||
|
||||
const x = computed(() => {
|
||||
return props.node.column * (props.boxWidth + props.marginX) + props.marginX / 2 + props.paddingX
|
||||
})
|
||||
return (
|
||||
props.node.column * (props.boxWidth + props.marginX) +
|
||||
props.marginX / 2 +
|
||||
props.paddingX
|
||||
);
|
||||
});
|
||||
|
||||
const y = computed(() => {
|
||||
return props.node.y * (props.boxHeight + props.marginY) + props.marginY / 2
|
||||
})
|
||||
return props.node.y * (props.boxHeight + props.marginY) + props.marginY / 2;
|
||||
});
|
||||
|
||||
const edges = computed(() => {
|
||||
const startX = x.value + props.boxWidth + 1
|
||||
const startY = y.value + props.boxHeight / 2
|
||||
const startX = x.value + props.boxWidth + 1;
|
||||
const startY = y.value + props.boxHeight / 2;
|
||||
return props.children.map((c) => {
|
||||
const endX = (c.column * (props.boxWidth + props.marginX) + props.marginX / 2 + props.paddingX) - 1
|
||||
const endY = (c.y * (props.boxHeight + props.marginY) + props.marginY / 2) + props.boxHeight / 2
|
||||
const endX =
|
||||
c.column * (props.boxWidth + props.marginX) +
|
||||
props.marginX / 2 +
|
||||
props.paddingX -
|
||||
1;
|
||||
const endY =
|
||||
c.y * (props.boxHeight + props.marginY) +
|
||||
props.marginY / 2 +
|
||||
props.boxHeight / 2;
|
||||
|
||||
return `
|
||||
M ${startX} ${startY}
|
||||
@@ -45,25 +56,25 @@ const edges = computed(() => {
|
||||
C ${endX - 25} ${startY} ${endX - 25} ${startY} ${endX - 25} ${startY + 25 * Math.sign(endY - startY)}
|
||||
L ${endX - 25} ${endY - 25 * Math.sign(endY - startY)}
|
||||
C ${endX - 25} ${endY} ${endX - 25} ${endY} ${endX} ${endY}
|
||||
`
|
||||
})
|
||||
})
|
||||
`;
|
||||
});
|
||||
});
|
||||
|
||||
const checkEdge = computed(() => {
|
||||
const startX = x.value + props.boxWidth + 1
|
||||
const startY = y.value + props.boxHeight / 2
|
||||
const startX = x.value + props.boxWidth + 1;
|
||||
const startY = y.value + props.boxHeight / 2;
|
||||
|
||||
return `M ${startX} ${startY} L ${startX + 25} ${startY}`
|
||||
})
|
||||
return `M ${startX} ${startY} L ${startX + 25} ${startY}`;
|
||||
});
|
||||
|
||||
const rootEdge = computed(() => {
|
||||
if (props.node.column > 0) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
const startX = 0
|
||||
const startY = props.boxHeight / 2 + props.marginY / 2
|
||||
const endX = x.value - 1
|
||||
const endY = y.value + props.boxHeight / 2
|
||||
const startX = 0;
|
||||
const startY = props.boxHeight / 2 + props.marginY / 2;
|
||||
const endX = x.value - 1;
|
||||
const endY = y.value + props.boxHeight / 2;
|
||||
|
||||
return `
|
||||
M ${startX} ${startY}
|
||||
@@ -71,98 +82,101 @@ const rootEdge = computed(() => {
|
||||
C ${endX - 25} ${startY} ${endX - 25} ${startY} ${endX - 25} ${startY + 25 * Math.sign(endY - startY)}
|
||||
L ${endX - 25} ${endY - 25 * Math.sign(endY - startY)}
|
||||
C ${endX - 25} ${endY} ${endX - 25} ${endY} ${endX} ${endY}
|
||||
`
|
||||
})
|
||||
`;
|
||||
});
|
||||
|
||||
const operator = computed(() => {
|
||||
return Object.keys(props.node.rule).filter((k) => !k.startsWith('__'))[0]
|
||||
})
|
||||
return Object.keys(props.node.rule).filter((k) => !k.startsWith("__"))[0];
|
||||
});
|
||||
|
||||
const variable = computed(() => {
|
||||
const op = operator.value
|
||||
const op = operator.value;
|
||||
if (props.node.rule[op] && props.node.rule[op][0]) {
|
||||
if (props.node.rule[op][0]['entries_since']) {
|
||||
return 'entries_since'
|
||||
if (props.node.rule[op][0]["entries_since"]) {
|
||||
return "entries_since";
|
||||
}
|
||||
if (props.node.rule[op][0]['entries_before']) {
|
||||
return 'entries_before'
|
||||
if (props.node.rule[op][0]["entries_before"]) {
|
||||
return "entries_before";
|
||||
}
|
||||
if (props.node.rule[op][0]['entries_days_since']) {
|
||||
return 'entries_days_since'
|
||||
if (props.node.rule[op][0]["entries_days_since"]) {
|
||||
return "entries_days_since";
|
||||
}
|
||||
if (props.node.rule[op][0]['entries_days_before']) {
|
||||
return 'entries_days_before'
|
||||
if (props.node.rule[op][0]["entries_days_before"]) {
|
||||
return "entries_days_before";
|
||||
}
|
||||
return props.node.rule[op][0]['var']
|
||||
return props.node.rule[op][0]["var"];
|
||||
} else {
|
||||
return ''
|
||||
return "";
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const vardata = computed(() => {
|
||||
return VARS[variable.value as keyof typeof VARS]
|
||||
})
|
||||
return VARS[variable.value as keyof typeof VARS];
|
||||
});
|
||||
|
||||
const varresult = computed(() => {
|
||||
const op = operator.value
|
||||
const op = operator.value;
|
||||
if (props.node.rule[op] && props.node.rule[op][0]) {
|
||||
if (typeof props.node.rule[op][0]['__result'] === 'undefined')
|
||||
return null
|
||||
return props.node.rule[op][0]['__result']
|
||||
if (typeof props.node.rule[op][0]["__result"] === "undefined") return null;
|
||||
return props.node.rule[op][0]["__result"];
|
||||
} else {
|
||||
return ''
|
||||
return "";
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const rightoperand = computed(() => {
|
||||
const op = operator.value
|
||||
if (props.node.rule[op] && typeof props.node.rule[op][1] !== 'undefined') {
|
||||
return props.node.rule[op][1]
|
||||
const op = operator.value;
|
||||
if (props.node.rule[op] && typeof props.node.rule[op][1] !== "undefined") {
|
||||
return props.node.rule[op][1];
|
||||
} else {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const op = computed(() => {
|
||||
return TYPEOPS[vardata.value.type as keyof typeof TYPEOPS]?.[operator.value as any]
|
||||
})
|
||||
return TYPEOPS[vardata.value.type as keyof typeof TYPEOPS]?.[
|
||||
operator.value as any
|
||||
];
|
||||
});
|
||||
|
||||
const operands = computed(() => {
|
||||
return props.node.rule[operator.value]
|
||||
})
|
||||
return props.node.rule[operator.value];
|
||||
});
|
||||
|
||||
const result = computed(() => {
|
||||
return typeof props.node.rule.__result === 'undefined' ? null : !!props.node.rule.__result
|
||||
})
|
||||
return typeof props.node.rule.__result === "undefined"
|
||||
? null
|
||||
: !!props.node.rule.__result;
|
||||
});
|
||||
|
||||
const resultInclParents = computed(() => {
|
||||
if (typeof props.node.rule.__result === 'undefined') return null
|
||||
if (typeof props.node.rule.__result === "undefined") return null;
|
||||
|
||||
function _p (node: GraphNode): boolean {
|
||||
function _p(node: GraphNode): boolean {
|
||||
if (node.parent) {
|
||||
return node.rule.__result && _p(node.parent)
|
||||
return node.rule.__result && _p(node.parent);
|
||||
}
|
||||
return node.rule.__result
|
||||
return node.rule.__result;
|
||||
}
|
||||
return _p(props.node)
|
||||
})
|
||||
return _p(props.node);
|
||||
});
|
||||
|
||||
const nodeClass = computed(() => {
|
||||
return {
|
||||
node: true,
|
||||
'node-true': result.value === true,
|
||||
'node-false': result.value === false,
|
||||
}
|
||||
})
|
||||
"node-true": result.value === true,
|
||||
"node-false": result.value === false,
|
||||
};
|
||||
});
|
||||
|
||||
function df (val: string) {
|
||||
const format = $('body').attr('data-datetimeformat')
|
||||
return moment(val).format(format)
|
||||
function df(val: string) {
|
||||
const format = $("body").attr("data-datetimeformat");
|
||||
return moment(val).format(format);
|
||||
}
|
||||
|
||||
function tf (val: string) {
|
||||
const format = $('body').attr('data-timeformat')
|
||||
return moment(val, 'HH:mm:ss').format(format)
|
||||
function tf(val: string) {
|
||||
const format = $("body").attr("data-timeformat");
|
||||
return moment(val, "HH:mm:ss").format(format);
|
||||
}
|
||||
</script>
|
||||
<template lang="pug">
|
||||
|
||||
@@ -1,95 +1,95 @@
|
||||
import type { Category, DayEntry, EventEntry, MetaFilterField } from '~/types'
|
||||
import type { Category, DayEntry, EventEntry, MetaFilterField } from "~/types";
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number
|
||||
responseUrl: string
|
||||
status: number;
|
||||
responseUrl: string;
|
||||
|
||||
constructor (status: number, responseUrl: string) {
|
||||
super(`HTTP ${status}`)
|
||||
this.status = status
|
||||
this.responseUrl = responseUrl
|
||||
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
|
||||
currency_places?: number
|
||||
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[]
|
||||
empty_text?: string
|
||||
weeks?: DayEntry[][]
|
||||
date?: string
|
||||
days?: DayEntry[]
|
||||
week?: [number, number]
|
||||
target_url?: string;
|
||||
subevent?: string | number;
|
||||
name?: string;
|
||||
frontpage_text?: string;
|
||||
date_range?: string;
|
||||
location?: string;
|
||||
items_by_category?: Category[];
|
||||
currency?: string;
|
||||
currency_places?: number;
|
||||
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[];
|
||||
empty_text?: string;
|
||||
weeks?: DayEntry[][];
|
||||
date?: string;
|
||||
days?: DayEntry[];
|
||||
week?: [number, number];
|
||||
}
|
||||
|
||||
export async function fetchProductList (url: string) {
|
||||
const response = await fetch(url)
|
||||
export async function fetchProductList(url: string) {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new ApiError(response.status, response.url)
|
||||
throw new ApiError(response.status, response.url);
|
||||
}
|
||||
return {
|
||||
data: await response.json() as ProductListResponse,
|
||||
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
|
||||
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) {
|
||||
export async function submitCart(endpoint: string, formData: FormData) {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
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)
|
||||
throw new ApiError(response.status, response.url);
|
||||
}
|
||||
return await response.json() as CartResponse
|
||||
return (await response.json()) as CartResponse;
|
||||
}
|
||||
|
||||
export async function checkAsyncTask (url: string) {
|
||||
const response = await fetch(url)
|
||||
export async function checkAsyncTask(url: string) {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new ApiError(response.status, response.url)
|
||||
throw new ApiError(response.status, response.url);
|
||||
}
|
||||
return await response.json() as CartResponse
|
||||
return (await response.json()) as CartResponse;
|
||||
}
|
||||
|
||||
export async function createCart (url: string) {
|
||||
const response = await fetch(url)
|
||||
export async function createCart(url: string) {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new ApiError(response.status, response.url)
|
||||
throw new ApiError(response.status, response.url);
|
||||
}
|
||||
return await response.json() as CartResponse
|
||||
return (await response.json()) as CartResponse;
|
||||
}
|
||||
@@ -1,31 +1,33 @@
|
||||
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'
|
||||
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
|
||||
export function createButtonInstance(element: Element, htmlId?: string): App {
|
||||
let targetUrl = element.attributes.event.value;
|
||||
if (!targetUrl.match(/\/$/)) {
|
||||
targetUrl += '/'
|
||||
targetUrl += "/";
|
||||
}
|
||||
|
||||
const widgetData: WidgetData = JSON.parse(JSON.stringify(window.PretixWidget.widget_data))
|
||||
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
|
||||
widgetData[attr.name.replace(/^data-/, "")] = attr.value;
|
||||
}
|
||||
}
|
||||
|
||||
const rawItems = element.attributes.items?.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 buttonItems: { item: string; count: string }[] = [];
|
||||
for (const itemStr of rawItems.split(",")) {
|
||||
if (itemStr.includes("=")) {
|
||||
const [item, count] = itemStr.split("=");
|
||||
buttonItems.push({ item, count });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,45 +35,50 @@ export function createButtonInstance (element: Element, htmlId?: string): App {
|
||||
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,
|
||||
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,
|
||||
keepCart: 'keep-cart' in element.attributes || buttonItems.length > 0,
|
||||
})
|
||||
keepCart: "keep-cart" in element.attributes || buttonItems.length > 0,
|
||||
});
|
||||
|
||||
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 (
|
||||
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
|
||||
store.widgetData[attrName] = attrValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const app = createApp(ButtonComponent)
|
||||
app.provide(StoreKey, store)
|
||||
const app = createApp(ButtonComponent);
|
||||
app.provide(StoreKey, store);
|
||||
app.config.errorHandler = (error, _vm, info) => {
|
||||
console.error('[pretix-button]', info, error)
|
||||
}
|
||||
console.error("[pretix-button]", info, error);
|
||||
};
|
||||
// Instead of mounting to the element directly, replicate vue2.7 behaviour, where
|
||||
// the HTML-element gets replaced instead of vue3’s behaviour to replace innerHTML.
|
||||
// The latter can cause issues with custom CSS as HTML structure would change.
|
||||
// app.mount(element)
|
||||
// observer.observe(element, { attributes: true })
|
||||
const fragment = document.createDocumentFragment()
|
||||
app.mount(fragment)
|
||||
const fragment = document.createDocumentFragment();
|
||||
app.mount(fragment);
|
||||
for (const attr of element.attributes) {
|
||||
fragment.firstChild.setAttribute(attr.name, attr.value)
|
||||
fragment.firstChild.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
observer.observe(fragment.firstChild, { attributes: true })
|
||||
element.parentNode.replaceChild(fragment, element)
|
||||
observer.observe(fragment.firstChild, { attributes: true });
|
||||
element.parentNode.replaceChild(fragment, element);
|
||||
|
||||
return app
|
||||
return app;
|
||||
}
|
||||
@@ -1,100 +1,120 @@
|
||||
<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'
|
||||
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
|
||||
}>()
|
||||
item: Item;
|
||||
variation?: Variation;
|
||||
}>();
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
const quantity = ref<HTMLInputElement>()
|
||||
const store = inject(StoreKey)!;
|
||||
const quantity = ref<HTMLInputElement>();
|
||||
|
||||
const avail = computed(() => props.item.has_variations ? props.variation.avail : props.item.avail)
|
||||
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 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 `variation_${props.item.id}_${props.variation.id}`;
|
||||
}
|
||||
return `item_${props.item.id}`
|
||||
})
|
||||
return `item_${props.item.id}`;
|
||||
});
|
||||
|
||||
const unavailabilityReasonMessage = computed(() => {
|
||||
const reason = props.item.current_unavailability_reason || props.variation?.current_unavailability_reason
|
||||
const reason =
|
||||
props.item.current_unavailability_reason ||
|
||||
props.variation?.current_unavailability_reason;
|
||||
if (reason) {
|
||||
return STRINGS[`unavailable_${reason}`] || reason
|
||||
return STRINGS[`unavailable_${reason}`] || reason;
|
||||
}
|
||||
return ''
|
||||
})
|
||||
return "";
|
||||
});
|
||||
|
||||
const voucherJumpLink = computed(() => `#${store.htmlId}-voucher-input`)
|
||||
const voucherJumpLink = computed(() => `#${store.htmlId}-voucher-input`);
|
||||
|
||||
const ariaLabelledby = computed(() => `${store.htmlId}-item-label-${props.item.id}`)
|
||||
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 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 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)
|
||||
})
|
||||
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 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}`
|
||||
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}`
|
||||
u += `&var=${props.variation.id}`;
|
||||
}
|
||||
if (store.subevent) {
|
||||
u += `&subevent=${store.subevent}`
|
||||
u += `&subevent=${store.subevent}`;
|
||||
}
|
||||
u += `&widget_data=${encodeURIComponent(store.widgetDataJson)}`
|
||||
u += store.consentParameter
|
||||
return u
|
||||
})
|
||||
u += `&widget_data=${encodeURIComponent(store.widgetDataJson)}`;
|
||||
u += store.consentParameter;
|
||||
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
|
||||
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 }))
|
||||
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.cartExists
|
||||
&& store.itemnum === 1
|
||||
&& (!store.categories[0]?.items[0]?.has_variations || store.categories[0]?.items[0]?.variations.length < 2)
|
||||
&& !store.hasSeatingPlan
|
||||
&& quantity.value
|
||||
!store.cartExists &&
|
||||
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
|
||||
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
|
||||
@@ -149,5 +169,4 @@ onMounted(() => {
|
||||
)
|
||||
span +
|
||||
</template>
|
||||
<style lang="sass">
|
||||
</style>
|
||||
<style lang="sass"></style>
|
||||
@@ -1,32 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref } from 'vue'
|
||||
import { StoreKey } from '~/sharedStore'
|
||||
import Overlay from './Overlay.vue'
|
||||
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 lang = LANG; // we need this so the template sees the variable
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
const store = inject(StoreKey)!;
|
||||
|
||||
const form = ref<HTMLFormElement>()
|
||||
const form = ref<HTMLFormElement>();
|
||||
|
||||
const formMethod = computed(() => {
|
||||
if (!store.useIframe && store.isButton && store.items.length === 0) {
|
||||
return 'get'
|
||||
return "get";
|
||||
}
|
||||
return 'post'
|
||||
})
|
||||
return "post";
|
||||
});
|
||||
|
||||
function handleBuy (event: Event) {
|
||||
function handleBuy(event: Event) {
|
||||
if (form.value) {
|
||||
const formData = new FormData(form.value)
|
||||
store.buy(formData, event)
|
||||
const formData = new FormData(form.value);
|
||||
store.buy(formData, event);
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
form,
|
||||
buy: handleBuy,
|
||||
})
|
||||
});
|
||||
</script>
|
||||
<template lang="pug">
|
||||
.pretix-widget-wrapper
|
||||
@@ -50,5 +50,4 @@ defineExpose({
|
||||
|
||||
Overlay
|
||||
</template>
|
||||
<style lang="sass">
|
||||
</style>
|
||||
<style lang="sass"></style>
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import type { Category } from '~/types'
|
||||
import Item from './Item.vue'
|
||||
import type { Category } from "~/types";
|
||||
import Item from "./Item.vue";
|
||||
|
||||
defineProps<{
|
||||
category: Category
|
||||
}>()
|
||||
category: Category;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
@@ -19,5 +19,4 @@ defineProps<{
|
||||
:category="category"
|
||||
)
|
||||
</template>
|
||||
<style lang="sass">
|
||||
</style>
|
||||
<style lang="sass"></style>
|
||||
@@ -1,66 +1,72 @@
|
||||
<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'
|
||||
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
|
||||
}>()
|
||||
mobile: boolean;
|
||||
}>();
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
const calendar = ref<HTMLDivElement>()
|
||||
const store = inject(StoreKey)!;
|
||||
const calendar = ref<HTMLDivElement>();
|
||||
|
||||
const displayEventInfo = computed(() => store.displayEventInfo || (store.displayEventInfo === null && store.parentStack.length > 0))
|
||||
const displayEventInfo = computed(
|
||||
() =>
|
||||
store.displayEventInfo ||
|
||||
(store.displayEventInfo === null && store.parentStack.length > 0),
|
||||
);
|
||||
|
||||
const monthname = computed(() => {
|
||||
// TODO proper date formatting?
|
||||
if (!store.date) return ''
|
||||
const monthNum = store.date.substr(5, 2)
|
||||
const year = store.date.substr(0, 4)
|
||||
return `${STRINGS.months[monthNum]} ${year}`
|
||||
})
|
||||
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 id = computed(() => `${store.htmlId}-event-calendar-table`);
|
||||
|
||||
const ariaLabelledby = computed(() => `${store.htmlId}-event-calendar-table-label`)
|
||||
const ariaLabelledby = computed(
|
||||
() => `${store.htmlId}-event-calendar-table-label`,
|
||||
);
|
||||
|
||||
function backToList () {
|
||||
function backToList() {
|
||||
// TODO should be in store
|
||||
store.weeks = null
|
||||
store.view = 'events'
|
||||
store.name = null
|
||||
store.frontpageText = null
|
||||
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--
|
||||
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--
|
||||
curMonth = 12;
|
||||
curYear--;
|
||||
}
|
||||
store.date = `${curYear}-${padNumber(curMonth, 2)}-01`
|
||||
store.loading++
|
||||
store.reload({ focus: `#${id.value}` })
|
||||
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++
|
||||
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++
|
||||
curMonth = 1;
|
||||
curYear++;
|
||||
}
|
||||
store.date = `${curYear}-${padNumber(curMonth, 2)}-01`
|
||||
store.loading++
|
||||
store.reload({ focus: `#${id.value}` })
|
||||
store.date = `${curYear}-${padNumber(curMonth, 2)}-01`;
|
||||
store.loading++;
|
||||
store.reload({ focus: `#${id.value}` });
|
||||
}
|
||||
</script>
|
||||
<template lang="pug">
|
||||
@@ -116,5 +122,4 @@ function nextmonth () {
|
||||
:mobile="mobile"
|
||||
)
|
||||
</template>
|
||||
<style lang="sass">
|
||||
</style>
|
||||
<style lang="sass"></style>
|
||||
@@ -1,108 +1,117 @@
|
||||
<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'
|
||||
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
|
||||
}>()
|
||||
day: DayEntry | null;
|
||||
mobile: boolean;
|
||||
}>();
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
const cellEl = ref<HTMLTableCellElement>()
|
||||
const store = inject(StoreKey)!;
|
||||
const cellEl = ref<HTMLTableCellElement>();
|
||||
|
||||
const daynum = computed(() => {
|
||||
if (!props.day) return ''
|
||||
return props.day.date.substr(8)
|
||||
})
|
||||
if (!props.day) return "";
|
||||
return props.day.date.substr(8);
|
||||
});
|
||||
|
||||
const dateStr = computed(() => props.day ? new Date(props.day.date).toLocaleDateString() : '')
|
||||
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 role = computed(() =>
|
||||
!props.day || !props.day.events.length || !props.mobile ? "cell" : "button",
|
||||
);
|
||||
|
||||
const tabindex = computed(() => role.value === 'button' ? '0' : '-1')
|
||||
const tabindex = computed(() => (role.value === "button" ? "0" : "-1"));
|
||||
|
||||
const classObject = computed(() => {
|
||||
const o: Record<string, boolean> = {}
|
||||
const o: Record<string, boolean> = {};
|
||||
if (props.day && props.day.events.length > 0) {
|
||||
o['pretix-widget-has-events'] = true // TODO static
|
||||
let best = 'red'
|
||||
let allLow = true
|
||||
o["pretix-widget-has-events"] = true; // TODO static
|
||||
let best = "red";
|
||||
let allLow = true;
|
||||
// TODO decopypasta
|
||||
for (const ev of props.day.events) {
|
||||
if (ev.availability.color === 'green') {
|
||||
best = 'green'
|
||||
if (ev.availability.reason !== 'low') {
|
||||
allLow = false
|
||||
if (ev.availability.color === "green") {
|
||||
best = "green";
|
||||
if (ev.availability.reason !== "low") {
|
||||
allLow = false;
|
||||
}
|
||||
} else if (ev.availability.color === 'orange' && best !== 'green') {
|
||||
best = 'orange'
|
||||
} 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
|
||||
o[`pretix-widget-day-availability-${best}`] = true;
|
||||
if (best === "green" && allLow) {
|
||||
o["pretix-widget-day-availability-low"] = true;
|
||||
}
|
||||
}
|
||||
return o
|
||||
})
|
||||
return o;
|
||||
});
|
||||
|
||||
function selectDay (e: Event) {
|
||||
if (!props.day || !props.day.events.length || !props.mobile) return
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
function selectDay(e: Event) {
|
||||
if (!props.day || !props.day.events.length || !props.mobile) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// TODO decopypasta
|
||||
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()
|
||||
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'
|
||||
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) {
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
const keyDown = e.key ?? e.keyCode;
|
||||
if (
|
||||
keyDown === "Enter" ||
|
||||
keyDown === 13 ||
|
||||
["Spacebar", " "].includes(keyDown as string) ||
|
||||
keyDown === 32
|
||||
) {
|
||||
// (prevent default so the page doesn't scroll when pressing space)
|
||||
e.preventDefault()
|
||||
selectDay(e)
|
||||
e.preventDefault();
|
||||
selectDay(e);
|
||||
}
|
||||
}
|
||||
|
||||
function attachListeners () {
|
||||
if (role.value === 'button' && cellEl.value) {
|
||||
cellEl.value.addEventListener('click', selectDay)
|
||||
cellEl.value.addEventListener('keydown', onKeyDown)
|
||||
function attachListeners() {
|
||||
if (role.value === "button" && cellEl.value) {
|
||||
cellEl.value.addEventListener("click", selectDay);
|
||||
cellEl.value.addEventListener("keydown", onKeyDown);
|
||||
}
|
||||
}
|
||||
|
||||
function detachListeners () {
|
||||
function detachListeners() {
|
||||
if (cellEl.value) {
|
||||
cellEl.value.removeEventListener('click', selectDay)
|
||||
cellEl.value.removeEventListener('keydown', onKeyDown)
|
||||
cellEl.value.removeEventListener("click", selectDay);
|
||||
cellEl.value.removeEventListener("keydown", onKeyDown);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
attachListeners()
|
||||
})
|
||||
attachListeners();
|
||||
});
|
||||
|
||||
// TODO why different from old version?
|
||||
watch(role, (newValue, oldValue) => {
|
||||
if (newValue === 'button' && oldValue !== 'button') {
|
||||
attachListeners()
|
||||
} else if (newValue !== 'button' && oldValue === 'button') {
|
||||
detachListeners()
|
||||
if (newValue === "button" && oldValue !== "button") {
|
||||
attachListeners();
|
||||
} else if (newValue !== "button" && oldValue === "button") {
|
||||
detachListeners();
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
<template lang="pug">
|
||||
td(
|
||||
@@ -116,5 +125,4 @@ td(
|
||||
.pretix-widget-event-calendar-events(v-if="day")
|
||||
EventCalendarEvent(v-for="e in day.events", :key="e.event_url+'-'+(e.subevent||'')", :event="e")
|
||||
</template>
|
||||
<style lang="sass">
|
||||
</style>
|
||||
<style lang="sass"></style>
|
||||
@@ -1,33 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
import type { EventEntry } from '~/types'
|
||||
import { StoreKey } from '~/sharedStore'
|
||||
import { computed, inject } from "vue";
|
||||
import type { EventEntry } from "~/types";
|
||||
import { StoreKey } from "~/sharedStore";
|
||||
|
||||
const props = defineProps<{
|
||||
event: EventEntry
|
||||
describedby?: string
|
||||
}>()
|
||||
event: EventEntry;
|
||||
describedby?: string;
|
||||
}>();
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
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
|
||||
"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
|
||||
o[`pretix-widget-event-availability-${props.event.availability.reason}`] =
|
||||
true;
|
||||
}
|
||||
return o
|
||||
})
|
||||
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()
|
||||
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">
|
||||
@@ -41,5 +43,4 @@ a.pretix-widget-event-calendar-event(
|
||||
.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>
|
||||
<style lang="sass"></style>
|
||||
@@ -1,15 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { DayEntry } from '~/types'
|
||||
import EventCalendarCell from './EventCalendarCell.vue'
|
||||
import type { DayEntry } from "~/types";
|
||||
import EventCalendarCell from "./EventCalendarCell.vue";
|
||||
|
||||
defineProps<{
|
||||
week: (DayEntry | null)[]
|
||||
mobile: boolean
|
||||
}>()
|
||||
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>
|
||||
<style lang="sass"></style>
|
||||
@@ -1,119 +1,148 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { StoreKey } from '~/sharedStore'
|
||||
import { STRINGS } from '~/i18n'
|
||||
import Category from './Category.vue'
|
||||
import {
|
||||
computed,
|
||||
inject,
|
||||
ref,
|
||||
watch,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
nextTick,
|
||||
} from "vue";
|
||||
import { StoreKey } from "~/sharedStore";
|
||||
import { STRINGS } from "~/i18n";
|
||||
import Category from "./Category.vue";
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
const store = inject(StoreKey)!;
|
||||
|
||||
const form = ref<HTMLFormElement>()
|
||||
const voucherinput = ref<HTMLInputElement>()
|
||||
const isItemsSelected = ref(false)
|
||||
const localVoucher = ref('')
|
||||
const form = ref<HTMLFormElement>();
|
||||
const voucherinput = ref<HTMLInputElement>();
|
||||
const isItemsSelected = ref(false);
|
||||
const localVoucher = ref("");
|
||||
|
||||
const idVoucherInput = computed(() => `${store.htmlId}-voucher-input`)
|
||||
const ariaLabelledby = computed(() => `${store.htmlId}-voucher-headline`)
|
||||
const displayEventInfo = computed(() => store.displayEventInfo || (store.displayEventInfo === null && (store.events || store.weeks || store.days)))
|
||||
const idCartExistsMsg = computed(() => `${store.htmlId}-cart-exists`)
|
||||
const idVoucherInput = computed(() => `${store.htmlId}-voucher-input`);
|
||||
const ariaLabelledby = computed(() => `${store.htmlId}-voucher-headline`);
|
||||
const displayEventInfo = computed(
|
||||
() =>
|
||||
store.displayEventInfo ||
|
||||
(store.displayEventInfo === null &&
|
||||
(store.events || store.weeks || store.days)),
|
||||
);
|
||||
const idCartExistsMsg = computed(() => `${store.htmlId}-cart-exists`);
|
||||
|
||||
const buyLabel = computed(() => {
|
||||
let allFree = true
|
||||
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 (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 (
|
||||
(item.variations.length === 0 && item.price.gross !== "0.00") ||
|
||||
item.mandatory_priced_addons
|
||||
) {
|
||||
allFree = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!allFree) break
|
||||
if (!allFree) break;
|
||||
}
|
||||
return allFree ? STRINGS.register : STRINGS.buy
|
||||
})
|
||||
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 [...params.entries()]
|
||||
})
|
||||
const params = new URL(store.getVoucherFormTarget()).searchParams;
|
||||
params.delete("iframe");
|
||||
params.delete("take_cart_id");
|
||||
return [...params.entries()];
|
||||
});
|
||||
|
||||
const showVoucherForm = computed(() => store.vouchersExist && !store.disableVouchers && !store.voucherCode)
|
||||
const showVoucherForm = computed(
|
||||
() => store.vouchersExist && !store.disableVouchers && !store.voucherCode,
|
||||
);
|
||||
|
||||
async function backToList () {
|
||||
store.targetUrl = store.parentStack.pop() || store.targetUrl
|
||||
store.error = null
|
||||
async function backToList() {
|
||||
store.targetUrl = store.parentStack.pop() || store.targetUrl;
|
||||
store.error = null;
|
||||
if (!store.subevent) {
|
||||
// reset if we are not in a series
|
||||
store.name = null
|
||||
store.frontpageText = null
|
||||
store.name = null;
|
||||
store.frontpageText = null;
|
||||
}
|
||||
store.subevent = null
|
||||
store.offset = 0
|
||||
store.appendEvents = false
|
||||
store.triggerLoadCallback()
|
||||
store.subevent = null;
|
||||
store.offset = 0;
|
||||
store.appendEvents = false;
|
||||
store.triggerLoadCallback();
|
||||
|
||||
if (store.events !== undefined && store.events !== null) {
|
||||
store.view = 'events'
|
||||
store.view = "events";
|
||||
} else if (store.days !== undefined && store.days !== null) {
|
||||
store.view = 'days'
|
||||
store.view = "days";
|
||||
} else {
|
||||
store.view = 'weeks'
|
||||
store.view = "weeks";
|
||||
}
|
||||
|
||||
// wait for redraw, then focus content element for better a11y
|
||||
const rootEl = form.value?.closest('.pretix-widget-wrapper') as HTMLElement | null
|
||||
await nextTick()
|
||||
rootEl?.focus()
|
||||
const rootEl = form.value?.closest(
|
||||
".pretix-widget-wrapper",
|
||||
) as HTMLElement | null;
|
||||
await nextTick();
|
||||
rootEl?.focus();
|
||||
}
|
||||
|
||||
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 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 _focusVoucherField() {
|
||||
voucherinput.value?.focus();
|
||||
}
|
||||
|
||||
function handleBuy (event: Event) {
|
||||
function handleBuy(event: Event) {
|
||||
if (form.value) {
|
||||
const formData = new FormData(form.value)
|
||||
store.buy(formData, event)
|
||||
const formData = new FormData(form.value);
|
||||
store.buy(formData, event);
|
||||
}
|
||||
}
|
||||
|
||||
function handleRedeem (event: Event) {
|
||||
store.redeem(localVoucher.value, event)
|
||||
function handleRedeem(event: Event) {
|
||||
store.redeem(localVoucher.value, event);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (form.value) {
|
||||
form.value.addEventListener('change', calcItemsSelected)
|
||||
form.value.addEventListener("change", calcItemsSelected);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (form.value) {
|
||||
form.value.removeEventListener('change', calcItemsSelected)
|
||||
form.value.removeEventListener("change", calcItemsSelected);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
watch(() => store.overlay?.frameShown, (newValue) => {
|
||||
if (!newValue && form.value) {
|
||||
form.value.reset()
|
||||
calcItemsSelected()
|
||||
}
|
||||
})
|
||||
watch(
|
||||
() => store.overlay?.frameShown,
|
||||
(newValue) => {
|
||||
if (!newValue && form.value) {
|
||||
form.value.reset();
|
||||
calcItemsSelected();
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
@@ -233,5 +262,4 @@ watch(() => store.overlay?.frameShown, (newValue) => {
|
||||
button(@click="handleRedeem") {{ STRINGS.redeem }}
|
||||
.pretix-widget-clear
|
||||
</template>
|
||||
<style lang="sass">
|
||||
</style>
|
||||
<style lang="sass"></style>
|
||||
@@ -1,44 +1,50 @@
|
||||
<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'
|
||||
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 store = inject(StoreKey)!;
|
||||
|
||||
const displayEventInfo = computed(() => store.displayEventInfo || (store.displayEventInfo === null && store.parentStack.length > 0))
|
||||
const displayEventInfo = computed(
|
||||
() =>
|
||||
store.displayEventInfo ||
|
||||
(store.displayEventInfo === null && store.parentStack.length > 0),
|
||||
);
|
||||
|
||||
async function backToCalendar (event: MouseEvent) {
|
||||
async function backToCalendar(event: MouseEvent) {
|
||||
// make sure to always focus content element
|
||||
await nextTick()
|
||||
const rootEl = (event.target as HTMLElement).closest('.pretix-widget-wrapper') as HTMLElement | null
|
||||
rootEl?.focus()
|
||||
await nextTick();
|
||||
const rootEl = (event.target as HTMLElement).closest(
|
||||
".pretix-widget-wrapper",
|
||||
) as HTMLElement | null;
|
||||
rootEl?.focus();
|
||||
|
||||
store.offset = 0
|
||||
store.appendEvents = false
|
||||
store.offset = 0;
|
||||
store.appendEvents = false;
|
||||
|
||||
if (store.weeks) {
|
||||
store.events = null
|
||||
store.view = 'weeks'
|
||||
store.name = null
|
||||
store.frontpageText = null
|
||||
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()
|
||||
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()
|
||||
function loadMore() {
|
||||
store.appendEvents = true;
|
||||
store.offset += 50;
|
||||
store.loading++;
|
||||
store.reload();
|
||||
}
|
||||
|
||||
console.log(store)
|
||||
console.log(store);
|
||||
</script>
|
||||
<template lang="pug">
|
||||
.pretix-widget-event-list
|
||||
@@ -67,5 +73,4 @@ console.log(store)
|
||||
p.pretix-widget-event-list-load-more(v-if="store.hasMoreEvents")
|
||||
button(@click.prevent.stop="loadMore") {{ STRINGS.load_more }}
|
||||
</template>
|
||||
<style lang="sass">
|
||||
</style>
|
||||
<style lang="sass"></style>
|
||||
@@ -1,34 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
import type { EventEntry } from '~/types'
|
||||
import { StoreKey } from '~/sharedStore'
|
||||
import { computed, inject } from "vue";
|
||||
import type { EventEntry } from "~/types";
|
||||
import { StoreKey } from "~/sharedStore";
|
||||
|
||||
const props = defineProps<{
|
||||
event: EventEntry
|
||||
}>()
|
||||
event: EventEntry;
|
||||
}>();
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
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
|
||||
"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
|
||||
o[`pretix-widget-event-availability-${props.event.availability.reason}`] =
|
||||
true;
|
||||
}
|
||||
return o
|
||||
})
|
||||
return o;
|
||||
});
|
||||
|
||||
const location = computed(() => props.event.location.replace(/\s*\n\s*/g, ', '))
|
||||
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()
|
||||
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">
|
||||
@@ -40,5 +44,4 @@ a.pretix-widget-event-list-entry(href="#", :class="classObject", @click.prevent.
|
||||
.pretix-widget-event-list-entry-availability
|
||||
span {{ event.availability.text }}
|
||||
</template>
|
||||
<style lang="sass">
|
||||
</style>
|
||||
<style lang="sass"></style>
|
||||
@@ -1,20 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
import type { MetaFilterField } from '~/types'
|
||||
import { StoreKey, globalWidgetId } from '~/sharedStore'
|
||||
import { computed, inject } from "vue";
|
||||
import type { MetaFilterField } from "~/types";
|
||||
import { StoreKey, globalWidgetId } from "~/sharedStore";
|
||||
|
||||
const props = defineProps<{
|
||||
field: MetaFilterField
|
||||
}>()
|
||||
field: MetaFilterField;
|
||||
}>();
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
const store = inject(StoreKey)!;
|
||||
|
||||
const id = computed(() => `${globalWidgetId}_${props.field.key}`)
|
||||
const id = computed(() => `${globalWidgetId}_${props.field.key}`);
|
||||
|
||||
const currentValue = computed(() => {
|
||||
const filterParams = new URLSearchParams(store.filter || '')
|
||||
return filterParams.get(props.field.key) || ''
|
||||
})
|
||||
const filterParams = new URLSearchParams(store.filter || "");
|
||||
return filterParams.get(props.field.key) || "";
|
||||
});
|
||||
</script>
|
||||
<template lang="pug">
|
||||
.pretix-widget-event-list-filter-field
|
||||
@@ -22,5 +22,4 @@ const currentValue = computed(() => {
|
||||
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>
|
||||
<style lang="sass"></style>
|
||||
@@ -1,28 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { inject, ref } from 'vue'
|
||||
import { StoreKey } from '~/sharedStore'
|
||||
import { STRINGS } from '~/i18n'
|
||||
import EventListFilterField from './EventListFilterField.vue'
|
||||
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>()
|
||||
const store = inject(StoreKey)!;
|
||||
const filterform = ref<HTMLFormElement>();
|
||||
|
||||
function onSubmit (e: Event) {
|
||||
e.preventDefault()
|
||||
if (!filterform.value) return
|
||||
function onSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
if (!filterform.value) return;
|
||||
|
||||
const formData = new FormData(filterform.value)
|
||||
const filterParams = new URLSearchParams()
|
||||
const formData = new FormData(filterform.value);
|
||||
const filterParams = new URLSearchParams();
|
||||
|
||||
formData.forEach((value, key) => {
|
||||
if (value !== '') {
|
||||
filterParams.set(key, value as string)
|
||||
if (value !== "") {
|
||||
filterParams.set(key, value as string);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
store.filter = filterParams.toString()
|
||||
store.loading++
|
||||
store.reload()
|
||||
store.filter = filterParams.toString();
|
||||
store.loading++;
|
||||
store.reload();
|
||||
}
|
||||
</script>
|
||||
<template lang="pug">
|
||||
@@ -36,5 +36,4 @@ form.pretix-widget-event-list-filter-form(ref="filterform", @submit="onSubmit")
|
||||
)
|
||||
button {{ STRINGS.filter }}
|
||||
</template>
|
||||
<style lang="sass">
|
||||
</style>
|
||||
<style lang="sass"></style>
|
||||
@@ -1,62 +1,66 @@
|
||||
<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'
|
||||
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
|
||||
}>()
|
||||
mobile: boolean;
|
||||
}>();
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
const weekcalendar = ref<HTMLDivElement>()
|
||||
const store = inject(StoreKey)!;
|
||||
const weekcalendar = ref<HTMLDivElement>();
|
||||
|
||||
const displayEventInfo = computed(() => store.displayEventInfo || (store.displayEventInfo === null && store.parentStack.length > 0))
|
||||
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}`
|
||||
})
|
||||
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 id = computed(() => `${store.htmlId}-event-week-table`);
|
||||
|
||||
function backToList () {
|
||||
store.weeks = null
|
||||
store.name = null
|
||||
store.frontpageText = null
|
||||
store.view = 'events'
|
||||
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--
|
||||
function prevweek() {
|
||||
if (!store.week) return;
|
||||
let curWeek = store.week[1];
|
||||
let curYear = store.week[0];
|
||||
curWeek--;
|
||||
if (curWeek < 1) {
|
||||
curYear--
|
||||
curWeek = getISOWeeks(curYear)
|
||||
curYear--;
|
||||
curWeek = getISOWeeks(curYear);
|
||||
}
|
||||
store.week = [curYear, curWeek]
|
||||
store.loading++
|
||||
store.reload({ focus: `#${id.value}` })
|
||||
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++
|
||||
function nextweek() {
|
||||
if (!store.week) return;
|
||||
let curWeek = store.week[1];
|
||||
let curYear = store.week[0];
|
||||
curWeek++;
|
||||
if (curWeek > getISOWeeks(curYear)) {
|
||||
curWeek = 1
|
||||
curYear++
|
||||
curWeek = 1;
|
||||
curYear++;
|
||||
}
|
||||
store.week = [curYear, curWeek]
|
||||
store.loading++
|
||||
store.reload({ focus: `#${id.value}` })
|
||||
store.week = [curYear, curWeek];
|
||||
store.loading++;
|
||||
store.reload({ focus: `#${id.value}` });
|
||||
}
|
||||
</script>
|
||||
<template lang="pug">
|
||||
@@ -93,5 +97,4 @@ function nextweek () {
|
||||
.pretix-widget-event-week-col(v-for="d in store.days", :key="d?.date || ''")
|
||||
EventWeekCell(:day="d", :mobile="mobile")
|
||||
</template>
|
||||
<style lang="sass">
|
||||
</style>
|
||||
<style lang="sass"></style>
|
||||
@@ -1,62 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
import type { DayEntry } from '~/types'
|
||||
import { StoreKey } from '~/sharedStore'
|
||||
import EventCalendarEvent from './EventCalendarEvent.vue'
|
||||
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 // TODO inject?
|
||||
}>()
|
||||
day: DayEntry | null;
|
||||
mobile: boolean; // TODO inject?
|
||||
}>();
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
const store = inject(StoreKey)!;
|
||||
|
||||
const id = computed(() => props.day ? `${store.htmlId}-${props.day.date}` : '')
|
||||
const id = computed(() =>
|
||||
props.day ? `${store.htmlId}-${props.day.date}` : "",
|
||||
);
|
||||
|
||||
const dayhead = computed(() => {
|
||||
if (!props.day) return ''
|
||||
return props.day.day_formatted
|
||||
})
|
||||
if (!props.day) return "";
|
||||
return props.day.day_formatted;
|
||||
});
|
||||
|
||||
const classObject = computed(() => {
|
||||
const o: Record<string, boolean> = {}
|
||||
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
|
||||
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
|
||||
if (ev.availability.color === "green") {
|
||||
best = "green";
|
||||
if (ev.availability.reason !== "low") {
|
||||
allLow = false;
|
||||
}
|
||||
} else if (ev.availability.color === 'orange' && best !== 'green') {
|
||||
best = 'orange'
|
||||
} 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
|
||||
o[`pretix-widget-day-availability-${best}`] = true;
|
||||
if (best === "green" && allLow) {
|
||||
o["pretix-widget-day-availability-low"] = true;
|
||||
}
|
||||
}
|
||||
return o
|
||||
})
|
||||
return o;
|
||||
});
|
||||
|
||||
function selectDay () {
|
||||
if (!props.day || !props.day.events.length || !props.mobile) return
|
||||
function selectDay() {
|
||||
if (!props.day || !props.day.events.length || !props.mobile) return;
|
||||
|
||||
if (props.day.events.length === 1) {
|
||||
const ev = props.day.events[0]
|
||||
const ev = props.day.events[0];
|
||||
// TODO store mutation bad
|
||||
store.parentStack.push(store.targetUrl)
|
||||
store.targetUrl = ev.event_url
|
||||
store.error = null
|
||||
store.subevent = ev.subevent ?? null
|
||||
store.loading++
|
||||
store.reload()
|
||||
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'
|
||||
store.events = props.day.events;
|
||||
store.view = "events";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -71,5 +73,4 @@ div(:class="classObject", @click.prevent.stop="selectDay")
|
||||
:describedby="id"
|
||||
)
|
||||
</template>
|
||||
<style lang="sass">
|
||||
</style>
|
||||
<style lang="sass"></style>
|
||||
@@ -1,69 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref, watch, onMounted } 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'
|
||||
import { computed, inject, ref, watch, onMounted } 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
|
||||
}>()
|
||||
item: Item;
|
||||
category: Category;
|
||||
}>();
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
const store = inject(StoreKey)!;
|
||||
|
||||
const expanded = ref(store.showVariationsExpanded)
|
||||
const variations = ref<HTMLDivElement>()
|
||||
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,
|
||||
}))
|
||||
"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,
|
||||
}))
|
||||
"pretix-widget-item-variations": true,
|
||||
"pretix-widget-item-variations-expanded": expanded.value,
|
||||
}));
|
||||
|
||||
const pictureAltText = computed(() => interpolate(STRINGS.image_of, [props.item.name]))
|
||||
const pictureAltText = computed(() =>
|
||||
interpolate(STRINGS.image_of, [props.item.name]),
|
||||
);
|
||||
|
||||
const headingLevel = computed(() => props.category.name ? '4' : '3')
|
||||
const headingLevel = computed(() => (props.category.name ? "4" : "3"));
|
||||
|
||||
const itemLabelId = computed(() => `${store.htmlId}-item-label-${props.item.id}`)
|
||||
const itemLabelId = computed(
|
||||
() => `${store.htmlId}-item-label-${props.item.id}`,
|
||||
);
|
||||
|
||||
const itemDescId = computed(() => `${store.htmlId}-item-desc-${props.item.id}`)
|
||||
const itemDescId = computed(() => `${store.htmlId}-item-desc-${props.item.id}`);
|
||||
|
||||
const itemPriceId = computed(() => `${store.htmlId}-item-price-${props.item.id}`)
|
||||
const itemPriceId = computed(
|
||||
() => `${store.htmlId}-item-price-${props.item.id}`,
|
||||
);
|
||||
|
||||
const ariaLabelledby = computed(() => `${itemLabelId.value} ${itemPriceId.value}`)
|
||||
const ariaLabelledby = computed(
|
||||
() => `${itemLabelId.value} ${itemPriceId.value}`,
|
||||
);
|
||||
|
||||
const minOrderStr = computed(() => interpolate(STRINGS.order_min, [props.item.order_min]))
|
||||
const minOrderStr = computed(() =>
|
||||
interpolate(STRINGS.order_min, [props.item.order_min]),
|
||||
);
|
||||
|
||||
const quotaLeftStr = computed(() => interpolate(STRINGS.quota_left, [props.item.avail[1]]))
|
||||
const quotaLeftStr = computed(() =>
|
||||
interpolate(STRINGS.quota_left, [props.item.avail[1]]),
|
||||
);
|
||||
|
||||
const showToggle = computed(() => props.item.has_variations && !store.showVariationsExpanded)
|
||||
const showToggle = computed(
|
||||
() => props.item.has_variations && !store.showVariationsExpanded,
|
||||
);
|
||||
|
||||
// TODO dedupe?
|
||||
const showPrices = computed(() => {
|
||||
let hasPriced = false
|
||||
let cntItems = 0
|
||||
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
|
||||
cntItems += item.variations.length;
|
||||
hasPriced = true;
|
||||
} else {
|
||||
cntItems++
|
||||
hasPriced = hasPriced || item.price.gross !== '0.00' || item.free_price
|
||||
cntItems++;
|
||||
hasPriced = hasPriced || item.price.gross !== "0.00" || item.free_price;
|
||||
}
|
||||
}
|
||||
}
|
||||
return hasPriced || cntItems > 1
|
||||
})
|
||||
return hasPriced || cntItems > 1;
|
||||
});
|
||||
|
||||
// TODO XSS
|
||||
const pricerange = computed(() => {
|
||||
@@ -72,68 +86,73 @@ const pricerange = computed(() => {
|
||||
STRINGS.price_from,
|
||||
{
|
||||
currency: store.currency,
|
||||
price: floatformat(props.item.min_price || '0', store.currency_places),
|
||||
price: floatformat(props.item.min_price || "0", store.currency_places),
|
||||
},
|
||||
true
|
||||
true,
|
||||
).replace(
|
||||
store.currency,
|
||||
`<span class="pretix-widget-pricebox-currency">${store.currency}</span>`
|
||||
)
|
||||
`<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', store.currency_places)} – ${floatformat(props.item.max_price || '0', store.currency_places)}`
|
||||
} else if (props.item.min_price === '0.00' && props.item.max_price === '0.00') {
|
||||
return `<span class="pretix-widget-pricebox-currency">${store.currency}</span> ${floatformat(props.item.min_price || "0", store.currency_places)} – ${floatformat(props.item.max_price || "0", store.currency_places)}`;
|
||||
} 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 "\xA0"; // nbsp, because an empty string would cause the HTML element to collapse
|
||||
}
|
||||
return STRINGS.free
|
||||
return STRINGS.free;
|
||||
} else {
|
||||
return `<span class="pretix-widget-pricebox-currency">${store.currency}</span> ${floatformat(props.item.min_price || '0', store.currency_places)}`
|
||||
return `<span class="pretix-widget-pricebox-currency">${store.currency}</span> ${floatformat(props.item.min_price || "0", store.currency_places)}`;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const variationsToggleLabel = computed(() => expanded.value ? STRINGS.hide_variations : STRINGS.variations)
|
||||
const variationsToggleLabel = computed(() =>
|
||||
expanded.value ? STRINGS.hide_variations : STRINGS.variations,
|
||||
);
|
||||
|
||||
function expand () {
|
||||
expanded.value = !expanded.value
|
||||
function expand() {
|
||||
expanded.value = !expanded.value;
|
||||
}
|
||||
|
||||
function lightbox () {
|
||||
function lightbox() {
|
||||
if (store.overlay) {
|
||||
store.overlay.lightbox = {
|
||||
image: props.item.picture_fullsize || '',
|
||||
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.hidden = true;
|
||||
|
||||
variations.value.addEventListener('transitionend', function (event) {
|
||||
variations.value.addEventListener("transitionend", function (event) {
|
||||
if (event.target === variations.value) {
|
||||
if (variations.value) {
|
||||
variations.value.hidden = !expanded.value
|
||||
variations.value.style.maxHeight = 'none'
|
||||
variations.value.hidden = !expanded.value;
|
||||
variations.value.style.maxHeight = "none";
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
watch(expanded, (newValue) => {
|
||||
const v = variations.value
|
||||
if (!v) return
|
||||
const v = variations.value;
|
||||
if (!v) return;
|
||||
|
||||
v.hidden = false
|
||||
v.style.maxHeight = `${newValue ? 0 : v.scrollHeight}px`
|
||||
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)
|
||||
})
|
||||
v.style.maxHeight = `${!newValue ? 0 : v.scrollHeight}px`;
|
||||
}, 50);
|
||||
});
|
||||
</script>
|
||||
<template lang="pug">
|
||||
div(
|
||||
@@ -214,5 +233,4 @@ div(
|
||||
:category="category"
|
||||
)
|
||||
</template>
|
||||
<style lang="sass">
|
||||
</style>
|
||||
<style lang="sass"></style>
|
||||
@@ -1,177 +1,206 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, onMounted, onUnmounted, inject, nextTick } from 'vue'
|
||||
import { StoreKey } from '~/sharedStore'
|
||||
import { STRINGS } from '~/i18n'
|
||||
import {
|
||||
computed,
|
||||
ref,
|
||||
watch,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
inject,
|
||||
nextTick,
|
||||
} from "vue";
|
||||
import { StoreKey } from "~/sharedStore";
|
||||
import { STRINGS } from "~/i18n";
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
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 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,
|
||||
}))
|
||||
"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,
|
||||
}))
|
||||
"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,
|
||||
}))
|
||||
"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,
|
||||
}))
|
||||
"pretix-widget-visibility-hidden": !cancelBlocked.value,
|
||||
}));
|
||||
|
||||
const errorMessageId = computed(() => `${store.htmlId}-error-message`)
|
||||
const errorMessageId = computed(() => `${store.htmlId}-error-message`);
|
||||
|
||||
function onMessage (e: MessageEvent) {
|
||||
if (e.data.type && e.data.type === 'pretix:widget:title') {
|
||||
function onMessage(e: MessageEvent) {
|
||||
if (e.data.type && e.data.type === "pretix:widget:title") {
|
||||
if (iframe.value) {
|
||||
iframe.value.title = e.data.title
|
||||
iframe.value.title = e.data.title;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function lightboxClose () {
|
||||
store.overlay.lightbox = null
|
||||
function lightboxClose() {
|
||||
store.overlay.lightbox = null;
|
||||
}
|
||||
|
||||
function lightboxLoaded () {
|
||||
function lightboxLoaded() {
|
||||
if (store.overlay.lightbox) {
|
||||
store.overlay.lightbox.loading = false
|
||||
store.overlay.lightbox.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function errorContinue () {
|
||||
function errorContinue() {
|
||||
if (store.overlay.errorUrlAfterNewTab) {
|
||||
window.open(store.overlay.errorUrlAfter)
|
||||
window.open(store.overlay.errorUrlAfter);
|
||||
} else {
|
||||
store.overlay.frameSrc = store.overlay.errorUrlAfter
|
||||
store.overlay.frameLoading = true
|
||||
store.overlay.frameSrc = store.overlay.errorUrlAfter;
|
||||
store.overlay.frameLoading = true;
|
||||
}
|
||||
}
|
||||
|
||||
function errorClose () {
|
||||
store.overlay.errorMessage = null
|
||||
store.overlay.errorUrlAfter = null
|
||||
store.overlay.errorUrlAfterNewTab = false
|
||||
function errorClose() {
|
||||
store.overlay.errorMessage = null;
|
||||
store.overlay.errorUrlAfter = null;
|
||||
store.overlay.errorUrlAfterNewTab = false;
|
||||
}
|
||||
|
||||
function close (e) {
|
||||
function close(e) {
|
||||
if (store.overlay.frameLoading) {
|
||||
// Chrome does not allow blocking dialog.cancel event more than once
|
||||
// => wiggle the loading-element and re-open the modal
|
||||
cancel(e)
|
||||
frameDialog.value?.showModal()
|
||||
return
|
||||
cancel(e);
|
||||
frameDialog.value?.showModal();
|
||||
return;
|
||||
}
|
||||
store.overlay.frameShown = false
|
||||
store.frameDismissed = true
|
||||
store.overlay.frameSrc = ''
|
||||
store.reload()
|
||||
triggerCloseCallback()
|
||||
store.overlay.frameShown = false;
|
||||
store.frameDismissed = true;
|
||||
store.overlay.frameSrc = "";
|
||||
store.reload();
|
||||
triggerCloseCallback();
|
||||
}
|
||||
|
||||
function cancel (e: Event) {
|
||||
function cancel(e: Event) {
|
||||
// do not allow to cancel while frame is loading as we cannot abort the operation
|
||||
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
|
||||
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 () {
|
||||
function iframeLoaded() {
|
||||
if (store.overlay.frameLoading) {
|
||||
store.overlay.frameLoading = false
|
||||
cancelBlocked.value = false
|
||||
store.overlay.frameLoading = false;
|
||||
cancelBlocked.value = false;
|
||||
if (store.overlay.frameSrc) {
|
||||
store.overlay.frameShown = true
|
||||
store.overlay.frameShown = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function triggerCloseCallback () {
|
||||
function triggerCloseCallback() {
|
||||
nextTick(() => {
|
||||
for (const callback of (window as any).PretixWidget._closed || []) {
|
||||
callback()
|
||||
callback();
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// TODO check if watchEffect is better for the following watchers
|
||||
watch(() => store.overlay.lightbox, (newValue, oldValue) => {
|
||||
if (!newValue) return
|
||||
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, async (newValue) => {
|
||||
if (!newValue) return
|
||||
await nextTick()
|
||||
closeButton.value?.focus()
|
||||
})
|
||||
|
||||
watch(() => store.overlay.frameSrc, (newValue, oldValue) => {
|
||||
// show loading spinner only when previously no frame_src was set
|
||||
if (newValue && !oldValue) {
|
||||
store.overlay.frameLoading = true
|
||||
}
|
||||
if (iframe.value) {
|
||||
// to close and unload the iframe, frame_src can be empty -> make it valid HTML with about:blank
|
||||
iframe.value.src = newValue || 'about:blank'
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => store.overlay.frameLoading, (newValue) => {
|
||||
if (newValue) {
|
||||
if (frameDialog.value && !frameDialog.value.open) {
|
||||
frameDialog.value.showModal()
|
||||
watch(
|
||||
() => store.overlay.lightbox,
|
||||
(newValue, oldValue) => {
|
||||
if (!newValue) return;
|
||||
if (newValue.image !== oldValue?.image) {
|
||||
newValue.loading = true;
|
||||
}
|
||||
} else {
|
||||
if (!store.overlay.frameSrc && frameDialog.value?.open) { // finished loading, but no iframe to display => close
|
||||
frameDialog.value.close()
|
||||
if (!oldValue) {
|
||||
lightboxDialog.value?.showModal();
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => store.overlay.errorMessage,
|
||||
(newValue, oldValue) => {
|
||||
if (newValue && !oldValue) {
|
||||
alertDialog.value?.showModal();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => store.overlay.frameShown,
|
||||
async (newValue) => {
|
||||
if (!newValue) return;
|
||||
await nextTick();
|
||||
closeButton.value?.focus();
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => store.overlay.frameSrc,
|
||||
(newValue, oldValue) => {
|
||||
// show loading spinner only when previously no frame_src was set
|
||||
if (newValue && !oldValue) {
|
||||
store.overlay.frameLoading = true;
|
||||
}
|
||||
if (iframe.value) {
|
||||
// to close and unload the iframe, frame_src can be empty -> make it valid HTML with about:blank
|
||||
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) {
|
||||
// finished loading, but no iframe to display => close
|
||||
frameDialog.value.close();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('message', onMessage, false)
|
||||
})
|
||||
window.addEventListener("message", onMessage, false);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('message', onMessage, false)
|
||||
})
|
||||
window.removeEventListener("message", onMessage, false);
|
||||
});
|
||||
|
||||
const vAutofocus = {
|
||||
mounted: (el) => el.focus()
|
||||
}
|
||||
mounted: (el) => el.focus(),
|
||||
};
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
@@ -231,5 +260,4 @@ Teleport(to="body")
|
||||
)
|
||||
figcaption(v-if="store.overlay.lightbox.description") {{ store.overlay.lightbox.description }}
|
||||
</template>
|
||||
<style lang="sass">
|
||||
</style>
|
||||
<style lang="sass"></style>
|
||||
@@ -1,93 +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'
|
||||
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
|
||||
}>()
|
||||
price: Price;
|
||||
freePrice: boolean;
|
||||
fieldName: string;
|
||||
suggestedPrice?: Price | null;
|
||||
originalPrice?: string | null;
|
||||
mandatoryPricedAddons?: boolean;
|
||||
itemId: number;
|
||||
}>();
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
const store = inject(StoreKey)!;
|
||||
|
||||
const priceBoxId = computed(() => `${store.htmlId}-item-pricebox-${props.itemId}`)
|
||||
const priceBoxId = computed(
|
||||
() => `${store.htmlId}-item-pricebox-${props.itemId}`,
|
||||
);
|
||||
|
||||
const priceDescId = computed(() => `${store.htmlId}-item-pricedesc-${props.itemId}`)
|
||||
const priceDescId = computed(
|
||||
() => `${store.htmlId}-item-pricedesc-${props.itemId}`,
|
||||
);
|
||||
|
||||
const ariaLabelledby = computed(() => `${store.htmlId}-item-label-${props.itemId} ${priceBoxId.value}`)
|
||||
const ariaLabelledby = computed(
|
||||
() => `${store.htmlId}-item-label-${props.itemId} ${priceBoxId.value}`,
|
||||
);
|
||||
|
||||
const displayPrice = computed(() => {
|
||||
if (store.displayNetPrices) {
|
||||
return floatformat(props.price.net, store.currency_places)
|
||||
return floatformat(props.price.net, store.currency_places);
|
||||
}
|
||||
return floatformat(props.price.gross, store.currency_places)
|
||||
})
|
||||
return floatformat(props.price.gross, store.currency_places);
|
||||
});
|
||||
|
||||
const displayPriceNonlocalized = computed(() => {
|
||||
if (store.displayNetPrices) {
|
||||
return parseFloat(props.price.net).toFixed(2)
|
||||
return parseFloat(props.price.net).toFixed(2);
|
||||
}
|
||||
return parseFloat(props.price.gross).toFixed(2)
|
||||
})
|
||||
return parseFloat(props.price.gross).toFixed(2);
|
||||
});
|
||||
|
||||
const suggestedPriceNonlocalized = computed(() => {
|
||||
const price = props.suggestedPrice ?? props.price
|
||||
const price = props.suggestedPrice ?? props.price;
|
||||
if (store.displayNetPrices) {
|
||||
return parseFloat(price.net).toFixed(store.currency_places)
|
||||
return parseFloat(price.net).toFixed(store.currency_places);
|
||||
}
|
||||
return parseFloat(price.gross).toFixed(store.currency_places)
|
||||
})
|
||||
return parseFloat(price.gross).toFixed(store.currency_places);
|
||||
});
|
||||
|
||||
// TODO BAD
|
||||
const originalLine = computed(() => {
|
||||
if (!props.originalPrice) return ''
|
||||
return `<span class="pretix-widget-pricebox-currency">${store.currency}</span> ${floatformat(props.originalPrice, store.currency_places)}`
|
||||
})
|
||||
if (!props.originalPrice) return "";
|
||||
return `<span class="pretix-widget-pricebox-currency">${store.currency}</span> ${floatformat(props.originalPrice, store.currency_places)}`;
|
||||
});
|
||||
|
||||
// TODO BAD
|
||||
const priceline = computed(() => {
|
||||
if (props.price.gross === '0.00') {
|
||||
if (props.price.gross === "0.00") {
|
||||
if (props.mandatoryPricedAddons && !props.originalPrice) {
|
||||
return '\u00A0' // nbsp
|
||||
return "\u00A0"; // nbsp
|
||||
}
|
||||
return STRINGS.free
|
||||
return STRINGS.free;
|
||||
}
|
||||
return `<span class="pretix-widget-pricebox-currency">${store.currency}</span> ${displayPrice.value}`
|
||||
})
|
||||
return `<span class="pretix-widget-pricebox-currency">${store.currency}</span> ${displayPrice.value}`;
|
||||
});
|
||||
|
||||
const originalPriceAriaLabel = computed(() => interpolate(STRINGS.original_price, [stripHTML(originalLine.value)]))
|
||||
const originalPriceAriaLabel = computed(() =>
|
||||
interpolate(STRINGS.original_price, [stripHTML(originalLine.value)]),
|
||||
);
|
||||
|
||||
const newPriceAriaLabel = computed(() => interpolate(STRINGS.new_price, [stripHTML(priceline.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 STRINGS.tax_plus_mixed;
|
||||
}
|
||||
return interpolate(STRINGS.tax_plus, {
|
||||
rate: autofloatformat(props.price.rate, 2),
|
||||
taxname: props.price.name,
|
||||
}, true)
|
||||
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 STRINGS.tax_incl_mixed;
|
||||
}
|
||||
return interpolate(STRINGS.tax_incl, {
|
||||
rate: autofloatformat(props.price.rate, 2),
|
||||
taxname: props.price.name,
|
||||
}, true)
|
||||
return interpolate(
|
||||
STRINGS.tax_incl,
|
||||
{
|
||||
rate: autofloatformat(props.price.rate, 2),
|
||||
taxname: props.price.name,
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// rate can either be "0.00" or "0" => parseFloat to check
|
||||
const showTaxline = computed(() => Number.parseFloat(props.price.rate) && Number.parseFloat(props.price.gross))
|
||||
const showTaxline = computed(
|
||||
() =>
|
||||
Number.parseFloat(props.price.rate) && Number.parseFloat(props.price.gross),
|
||||
);
|
||||
</script>
|
||||
<template lang="pug">
|
||||
.pretix-widget-pricebox
|
||||
@@ -111,5 +132,4 @@ const showTaxline = computed(() => Number.parseFloat(props.price.rate) && Number
|
||||
)
|
||||
small.pretix-widget-pricebox-tax(v-if="showTaxline", :id="priceDescId") {{ taxline }}
|
||||
</template>
|
||||
<style lang="sass">
|
||||
</style>
|
||||
<style lang="sass"></style>
|
||||
@@ -1,53 +1,69 @@
|
||||
<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'
|
||||
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
|
||||
}>()
|
||||
variation: Variation;
|
||||
item: Item;
|
||||
category: Category;
|
||||
}>();
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
const store = inject(StoreKey)!;
|
||||
|
||||
const origPrice = computed(() => props.variation.original_price || props.item.original_price)
|
||||
const origPrice = computed(
|
||||
() => props.variation.original_price || props.item.original_price,
|
||||
);
|
||||
|
||||
const quotaLeftStr = computed(() => interpolate(STRINGS.quota_left, [props.variation.avail[1]]))
|
||||
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 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 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 variationPriceId = computed(
|
||||
() =>
|
||||
`${store.htmlId}-variation-price-${props.item.id}-${props.variation.id}`,
|
||||
);
|
||||
|
||||
const ariaLabelledby = computed(() => `${variationLabelId.value} ${variationPriceId.value}`)
|
||||
const ariaLabelledby = computed(
|
||||
() => `${variationLabelId.value} ${variationPriceId.value}`,
|
||||
);
|
||||
|
||||
const headingLevel = computed(() => props.category.name ? '5' : '4')
|
||||
const headingLevel = computed(() => (props.category.name ? "5" : "4"));
|
||||
|
||||
const showQuotaLeft = computed(() => props.variation.avail[1] !== null && props.variation.avail[0] === 100)
|
||||
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
|
||||
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
|
||||
cntItems += item.variations.length;
|
||||
hasPriced = true;
|
||||
} else {
|
||||
cntItems++
|
||||
hasPriced = hasPriced || item.price.gross !== '0.00' || item.free_price
|
||||
cntItems++;
|
||||
hasPriced = hasPriced || item.price.gross !== "0.00" || item.free_price;
|
||||
}
|
||||
}
|
||||
}
|
||||
return hasPriced || cntItems > 1
|
||||
})
|
||||
return hasPriced || cntItems > 1;
|
||||
});
|
||||
</script>
|
||||
<template lang="pug">
|
||||
.pretix-widget-variation(
|
||||
@@ -93,5 +109,4 @@ const showPrices = computed(() => {
|
||||
|
||||
.pretix-widget-clear
|
||||
</template>
|
||||
<style lang="sass">
|
||||
</style>
|
||||
<style lang="sass"></style>
|
||||
@@ -1,55 +1,58 @@
|
||||
<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'
|
||||
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: []
|
||||
}>()
|
||||
mounted: [];
|
||||
}>();
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
const store = inject(StoreKey)!;
|
||||
|
||||
const wrapper = ref<HTMLDivElement>()
|
||||
const formcomp = ref<InstanceType<typeof EventForm>>()
|
||||
const mobile = ref(false)
|
||||
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,
|
||||
}))
|
||||
"pretix-widget": true,
|
||||
"pretix-widget-mobile": mobile.value,
|
||||
"pretix-widget-use-custom-spinners": true,
|
||||
}));
|
||||
|
||||
watch(mobile, (newValue) => {
|
||||
store.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)
|
||||
mobile.value = entries[0].contentRect.width <= 800;
|
||||
});
|
||||
resizeObserver.observe(wrapper.value);
|
||||
}
|
||||
|
||||
store.reload() // TODO call earlier?
|
||||
emit('mounted') // TODO where does this go?
|
||||
})
|
||||
store.reload(); // TODO call earlier?
|
||||
emit("mounted"); // TODO where does this go?
|
||||
});
|
||||
|
||||
watch(() => store.view, (_newValue, oldValue) => {
|
||||
if (oldValue && wrapper.value) {
|
||||
// always make sure the widget is scrolled to the top
|
||||
// as we only check top, we do not need to wait for a redraw
|
||||
const rect = wrapper.value.getBoundingClientRect()
|
||||
if (rect.top < 0) {
|
||||
wrapper.value.scrollIntoView()
|
||||
watch(
|
||||
() => store.view,
|
||||
(_newValue, oldValue) => {
|
||||
if (oldValue && wrapper.value) {
|
||||
// always make sure the widget is scrolled to the top
|
||||
// as we only check top, we do not need to wait for a redraw
|
||||
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")
|
||||
@@ -72,5 +75,4 @@ watch(() => store.view, (_newValue, oldValue) => {
|
||||
|
||||
Overlay
|
||||
</template>
|
||||
<style lang="sass">
|
||||
</style>
|
||||
<style lang="sass"></style>
|
||||
@@ -5,122 +5,164 @@
|
||||
// with a local context.
|
||||
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
let django: Django
|
||||
let django: Django;
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
// TODO this does not actually grab the correct language strings
|
||||
const modules = import.meta.glob('../../../jsi18n/*/djangojs.js', { eager: true, query: '?raw', import: 'default' }) as Record<string, string>
|
||||
const key = `../../../jsi18n/${LANG}/djangojs.js`
|
||||
const raw = modules[key]
|
||||
if (!raw) throw new Error(`Missing i18n module for lang "${LANG}": ${key}`)
|
||||
const context: { django?: Django } = {}
|
||||
new Function(raw).call(context)
|
||||
django = context.django!
|
||||
const modules = import.meta.glob("../../../jsi18n/*/djangojs.js", {
|
||||
eager: true,
|
||||
query: "?raw",
|
||||
import: "default",
|
||||
}) as Record<string, string>;
|
||||
const key = `../../../jsi18n/${LANG}/djangojs.js`;
|
||||
const raw = modules[key];
|
||||
if (!raw) throw new Error(`Missing i18n module for lang "${LANG}": ${key}`);
|
||||
const context: { django?: Django } = {};
|
||||
new Function(raw).call(context);
|
||||
django = context.django!;
|
||||
} else {
|
||||
django = (globalThis as any).django
|
||||
django = (globalThis as any).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'),
|
||||
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'),
|
||||
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'),
|
||||
"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
|
||||
} as const;
|
||||
|
||||
export function interpolate (fmt: string, obj: Record<string, unknown> | unknown[], named = false): string {
|
||||
return django.interpolate(fmt, obj, named)
|
||||
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)
|
||||
export function getFormat(formatType: string): string | number {
|
||||
return django.get_format(formatType);
|
||||
}
|
||||
@@ -1,80 +1,96 @@
|
||||
import { reactive, computed, watch } from 'vue'
|
||||
import type { WatchCallback, WatchOptions, UnwrapNestedRefs } from 'vue'
|
||||
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
|
||||
$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
|
||||
}
|
||||
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>
|
||||
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
|
||||
> (
|
||||
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>
|
||||
}
|
||||
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
|
||||
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))
|
||||
const getter = config.getters[key];
|
||||
const computedRef = computed(() => (getter as () => unknown).call(store));
|
||||
Object.defineProperty(store, key, {
|
||||
get: () => computedRef.value,
|
||||
enumerable: true
|
||||
})
|
||||
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)
|
||||
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 cleanState = config.state();
|
||||
const cleanKeys = new Set([
|
||||
'$reset',
|
||||
'$watch',
|
||||
"$reset",
|
||||
"$watch",
|
||||
...Object.keys(cleanState),
|
||||
...Object.keys(config.getters ?? {}),
|
||||
...Object.keys(config.actions ?? {})
|
||||
])
|
||||
...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]
|
||||
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 as S)[key] = cleanState[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
store.$watch = function <T>(source: () => T, callback: WatchCallback<T>, options?: WatchOptions) {
|
||||
watch(source.bind(store), callback.bind(store), options)
|
||||
}
|
||||
store.$watch = function <T>(
|
||||
source: () => T,
|
||||
callback: WatchCallback<T>,
|
||||
options?: WatchOptions,
|
||||
) {
|
||||
watch(source.bind(store), callback.bind(store), options);
|
||||
};
|
||||
|
||||
return store
|
||||
return store;
|
||||
}
|
||||
@@ -1,22 +1,22 @@
|
||||
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'
|
||||
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
|
||||
PretixWidget: PretixWidgetAPI;
|
||||
pretixWidgetCallback?: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
interface PretixWidgetAPI {
|
||||
build_widgets: boolean
|
||||
widget_data: WidgetData
|
||||
buildWidgets: () => void
|
||||
build_widgets: boolean;
|
||||
widget_data: WidgetData;
|
||||
buildWidgets: () => void;
|
||||
open: (
|
||||
targetUrl: string,
|
||||
voucher?: string | null,
|
||||
@@ -24,16 +24,16 @@ interface PretixWidgetAPI {
|
||||
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>
|
||||
disableIframe?: boolean,
|
||||
) => void;
|
||||
addLoadListener: (callback: () => void) => void;
|
||||
addCloseListener: (callback: () => void) => void;
|
||||
_loaded: Array<() => void>;
|
||||
_closed: Array<() => void>;
|
||||
}
|
||||
|
||||
const widgetlist: App[] = []
|
||||
const buttonlist: App[] = []
|
||||
const widgetlist: App[] = [];
|
||||
const buttonlist: App[] = [];
|
||||
|
||||
window.PretixWidget = {
|
||||
build_widgets: true,
|
||||
@@ -43,43 +43,53 @@ window.PretixWidget = {
|
||||
_closed: [],
|
||||
buildWidgets,
|
||||
open: openWidget,
|
||||
addLoadListener (f) { this._loaded.push(f) },
|
||||
addCloseListener (f) { this._closed.push(f) },
|
||||
}
|
||||
addLoadListener(f) {
|
||||
this._loaded.push(f);
|
||||
},
|
||||
addCloseListener(f) {
|
||||
this._closed.push(f);
|
||||
},
|
||||
};
|
||||
|
||||
async function buildWidgets () {
|
||||
await docReady()
|
||||
const widgetElements = document.querySelectorAll('pretix-widget, div.pretix-widget-compat')
|
||||
async function buildWidgets() {
|
||||
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}`))
|
||||
widgetlist.push(createWidgetInstance(el, el.id || `pretix-widget-${i}`));
|
||||
}
|
||||
const buttonElements = document.querySelectorAll('pretix-button, div.pretix-button-compat')
|
||||
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}`))
|
||||
buttonlist.push(createButtonInstance(el, el.id || `pretix-button-${i}`));
|
||||
}
|
||||
}
|
||||
|
||||
function openWidget (
|
||||
function openWidget(
|
||||
targetUrl: string,
|
||||
voucher?: string | null,
|
||||
subevent?: string | number | null,
|
||||
items?: { item: string; count: string }[],
|
||||
widgetData?: Record<string, string>,
|
||||
skipSslCheck?: boolean,
|
||||
disableIframe?: boolean
|
||||
disableIframe?: boolean,
|
||||
): void {
|
||||
if (!targetUrl.match(/\/$/)) {
|
||||
targetUrl += '/'
|
||||
targetUrl += "/";
|
||||
}
|
||||
|
||||
const allWidgetData: WidgetData = JSON.parse(JSON.stringify(window.PretixWidget.widget_data))
|
||||
const allWidgetData: WidgetData = JSON.parse(
|
||||
JSON.stringify(window.PretixWidget.widget_data),
|
||||
);
|
||||
if (widgetData) {
|
||||
Object.assign(allWidgetData, widgetData)
|
||||
Object.assign(allWidgetData, widgetData);
|
||||
}
|
||||
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
root.classList.add('pretix-widget-hidden')
|
||||
const root = document.createElement("div");
|
||||
document.body.appendChild(root);
|
||||
root.classList.add("pretix-widget-hidden");
|
||||
|
||||
const store = createWidgetStore({
|
||||
targetUrl,
|
||||
@@ -91,37 +101,37 @@ function openWidget (
|
||||
htmlId: makeid(16),
|
||||
isButton: true,
|
||||
buttonItems: items ?? [],
|
||||
buttonText: '',
|
||||
keepCart: true
|
||||
})
|
||||
buttonText: "",
|
||||
keepCart: true,
|
||||
});
|
||||
|
||||
const app = createApp(ButtonComponent)
|
||||
app.provide(StoreKey, store)
|
||||
const app = createApp(ButtonComponent);
|
||||
app.provide(StoreKey, store);
|
||||
app.config.errorHandler = (error, _vm, info) => {
|
||||
console.error('[pretix-widget-open]', info, error)
|
||||
}
|
||||
app.mount(root)
|
||||
console.error("[pretix-widget-open]", info, error);
|
||||
};
|
||||
app.mount(root);
|
||||
|
||||
nextTick(() => {
|
||||
if (store.useIframe) {
|
||||
const form = root.querySelector('form') as HTMLFormElement
|
||||
const form = root.querySelector("form") as HTMLFormElement;
|
||||
if (form) {
|
||||
const formData = new FormData(form)
|
||||
store.buy(formData)
|
||||
const formData = new FormData(form);
|
||||
store.buy(formData);
|
||||
}
|
||||
} else {
|
||||
const form = root.querySelector('form') as HTMLFormElement
|
||||
if (form) form.submit()
|
||||
const form = root.querySelector("form") as HTMLFormElement;
|
||||
if (form) form.submit();
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof window.pretixWidgetCallback !== 'undefined') {
|
||||
window.pretixWidgetCallback()
|
||||
if (typeof window.pretixWidgetCallback !== "undefined") {
|
||||
window.pretixWidgetCallback();
|
||||
}
|
||||
|
||||
if (window.PretixWidget.build_widgets) {
|
||||
window.PretixWidget.buildWidgets()
|
||||
window.PretixWidget.buildWidgets();
|
||||
}
|
||||
|
||||
// TODO debug exposes
|
||||
@@ -1,64 +1,77 @@
|
||||
import { nextTick, type InjectionKey } from 'vue'
|
||||
import { createStore } from '~/lib/store'
|
||||
import { fetchProductList, submitCart, checkAsyncTask, ApiError, createCart } from '~/api'
|
||||
import type { CartResponse } from '~/api'
|
||||
import { STRINGS } from '~/i18n'
|
||||
import { setCookie, getCookie, makeid, siteIsSecure } from '~/utils'
|
||||
import type { Category, DayEntry, EventEntry, LightboxState, MetaFilterField, WidgetData } from '~/types'
|
||||
import { nextTick, type InjectionKey } from "vue";
|
||||
import { createStore } from "~/lib/store";
|
||||
import {
|
||||
fetchProductList,
|
||||
submitCart,
|
||||
checkAsyncTask,
|
||||
ApiError,
|
||||
createCart,
|
||||
} from "~/api";
|
||||
import type { CartResponse } from "~/api";
|
||||
import { STRINGS } from "~/i18n";
|
||||
import { setCookie, getCookie, makeid, siteIsSecure } from "~/utils";
|
||||
import type {
|
||||
Category,
|
||||
DayEntry,
|
||||
EventEntry,
|
||||
LightboxState,
|
||||
MetaFilterField,
|
||||
WidgetData,
|
||||
} from "~/types";
|
||||
|
||||
export const globalWidgetId = makeid(16)
|
||||
export const globalWidgetId = makeid(16);
|
||||
|
||||
export type WidgetStore = ReturnType<typeof createWidgetStore>
|
||||
export const StoreKey: InjectionKey<WidgetStore> = Symbol('WidgetStore')
|
||||
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
|
||||
keepCart: boolean
|
||||
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;
|
||||
keepCart: boolean;
|
||||
// Button-specific
|
||||
buttonItems?: { item: string; count: string }[]
|
||||
buttonText?: string
|
||||
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,
|
||||
subevent: config.subevent ?? (null as string | number | null),
|
||||
|
||||
// Configuration
|
||||
voucherCode: config.voucher ?? null as string | null,
|
||||
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,
|
||||
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,
|
||||
keepCart: config.keepCart,
|
||||
|
||||
// View state
|
||||
view: null as 'event' | 'events' | 'weeks' | 'days' | null,
|
||||
view: null as "event" | "events" | "weeks" | "days" | null,
|
||||
loading: config.isButton ? 0 : 1,
|
||||
error: null as string | null,
|
||||
connectionError: false,
|
||||
@@ -70,7 +83,7 @@ export function createWidgetStore (config: {
|
||||
location: null as string | null,
|
||||
frontpageText: null as string | null,
|
||||
categories: [] as Category[],
|
||||
currency: '',
|
||||
currency: "",
|
||||
currency_places: 2,
|
||||
displayNetPrices: false,
|
||||
voucherExplanationText: null as string | null,
|
||||
@@ -83,7 +96,7 @@ export function createWidgetStore (config: {
|
||||
hasSeatingPlan: false,
|
||||
hasSeatingPlanWaitinglist: false,
|
||||
itemnum: 0,
|
||||
poweredby: '',
|
||||
poweredby: "",
|
||||
|
||||
// Calendar/list data
|
||||
events: null as EventEntry[] | null,
|
||||
@@ -103,11 +116,11 @@ export function createWidgetStore (config: {
|
||||
// Button-specific
|
||||
isButton: config.isButton ?? false,
|
||||
items: (config.buttonItems ?? []) as { item: string; count: string }[],
|
||||
buttonText: config.buttonText ?? '',
|
||||
buttonText: config.buttonText ?? "",
|
||||
|
||||
// Overlay (always initialized, no null guards)
|
||||
overlay: {
|
||||
frameSrc: '',
|
||||
frameSrc: "",
|
||||
frameLoading: false,
|
||||
frameShown: false,
|
||||
errorMessage: null as string | null,
|
||||
@@ -125,456 +138,490 @@ export function createWidgetStore (config: {
|
||||
}),
|
||||
|
||||
getters: {
|
||||
useIframe (): boolean {
|
||||
if ((window as any).crossOriginIsolated === true) return false
|
||||
return !this.disableIframe && (this.skipSsl || siteIsSecure())
|
||||
useIframe(): boolean {
|
||||
if ((window as any).crossOriginIsolated === true) return false;
|
||||
return !this.disableIframe && (this.skipSsl || siteIsSecure());
|
||||
},
|
||||
cookieName (): string {
|
||||
return `pretix_widget_${this.targetUrl.replace(/[^a-zA-Z0-9]+/g, '_')}`
|
||||
cookieName(): string {
|
||||
return `pretix_widget_${this.targetUrl.replace(/[^a-zA-Z0-9]+/g, "_")}`;
|
||||
},
|
||||
cartId (): string | null {
|
||||
cartId(): string | null {
|
||||
if (this._cartId) {
|
||||
return this._cartId
|
||||
return this._cartId;
|
||||
}
|
||||
if (this.keepCart) {
|
||||
return getCookie(this.cookieName) ?? null
|
||||
return getCookie(this.cookieName) ?? null;
|
||||
}
|
||||
},
|
||||
widgetDataJson (): string {
|
||||
const cloned = { ...this.widgetData }
|
||||
delete cloned.consent
|
||||
return JSON.stringify(cloned)
|
||||
widgetDataJson(): string {
|
||||
const cloned = { ...this.widgetData };
|
||||
delete cloned.consent;
|
||||
return JSON.stringify(cloned);
|
||||
},
|
||||
consentParameter (): string {
|
||||
consentParameter(): string {
|
||||
if (this.widgetData.consent) {
|
||||
return `&consent=${encodeURIComponent(this.widgetData.consent)}`
|
||||
return `&consent=${encodeURIComponent(this.widgetData.consent)}`;
|
||||
}
|
||||
return ''
|
||||
return "";
|
||||
},
|
||||
additionalURLParams (): string {
|
||||
if (!window.location.search.includes('utm_')) {
|
||||
return ''
|
||||
additionalURLParams(): string {
|
||||
if (!window.location.search.includes("utm_")) {
|
||||
return "";
|
||||
}
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
for (const [key] of params.entries()) {
|
||||
if (!key.startsWith('utm_')) {
|
||||
params.delete(key)
|
||||
if (!key.startsWith("utm_")) {
|
||||
params.delete(key);
|
||||
}
|
||||
}
|
||||
return params.toString()
|
||||
return params.toString();
|
||||
},
|
||||
newTabTarget (): string {
|
||||
let url = this.subevent ? `${this.targetUrl}${this.subevent}/` : this.targetUrl
|
||||
let parameters = this.consentParameter
|
||||
newTabTarget(): string {
|
||||
let url = this.subevent
|
||||
? `${this.targetUrl}${this.subevent}/`
|
||||
: this.targetUrl;
|
||||
let parameters = this.consentParameter;
|
||||
if (this.additionalURLParams) {
|
||||
parameters += `&${this.additionalURLParams}`
|
||||
parameters += `&${this.additionalURLParams}`;
|
||||
}
|
||||
if (parameters) {
|
||||
url += '?' + parameters.replace(/^&/, '')
|
||||
url += "?" + parameters.replace(/^&/, "");
|
||||
}
|
||||
return url
|
||||
return url;
|
||||
},
|
||||
formTarget (): string {
|
||||
const isFirefox = navigator.userAgent.toLowerCase().includes('firefox')
|
||||
const isAndroid = navigator.userAgent.toLowerCase().includes('android')
|
||||
formTarget(): string {
|
||||
const isFirefox = navigator.userAgent.toLowerCase().includes("firefox");
|
||||
const isAndroid = navigator.userAgent.toLowerCase().includes("android");
|
||||
if (isAndroid && isFirefox) {
|
||||
return '_top'
|
||||
return "_top";
|
||||
}
|
||||
return '_blank'
|
||||
return "_blank";
|
||||
},
|
||||
consentParameterValue (): string {
|
||||
consentParameterValue(): string {
|
||||
if (this.widgetData.consent) {
|
||||
return encodeURIComponent(this.widgetData.consent)
|
||||
return encodeURIComponent(this.widgetData.consent);
|
||||
}
|
||||
return ''
|
||||
return "";
|
||||
},
|
||||
formAction (): string {
|
||||
formAction(): 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
|
||||
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}/`
|
||||
let checkoutUrl = `/${this.targetUrl.replace(/^[^/]+:\/\/([^/]+)\//, "")}w/${globalWidgetId}/`;
|
||||
if (!this.cartExists) {
|
||||
checkoutUrl += 'checkout/start'
|
||||
checkoutUrl += "checkout/start";
|
||||
}
|
||||
if (this.additionalURLParams) {
|
||||
checkoutUrl += `?${this.additionalURLParams}`
|
||||
checkoutUrl += `?${this.additionalURLParams}`;
|
||||
}
|
||||
|
||||
let formTarget = `${this.targetUrl}w/${globalWidgetId}/cart/add?iframe=1&next=${encodeURIComponent(checkoutUrl)}`
|
||||
let formTarget = `${this.targetUrl}w/${globalWidgetId}/cart/add?iframe=1&next=${encodeURIComponent(checkoutUrl)}`;
|
||||
if (this.cartId) {
|
||||
formTarget += `&take_cart_id=${this.cartId}`
|
||||
formTarget += `&take_cart_id=${this.cartId}`;
|
||||
}
|
||||
formTarget += this.consentParameter
|
||||
return formTarget
|
||||
}
|
||||
formTarget += this.consentParameter;
|
||||
return formTarget;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
triggerLoadCallback () {
|
||||
triggerLoadCallback() {
|
||||
nextTick(() => {
|
||||
for (const callback of (window as any).PretixWidget._loaded || []) {
|
||||
callback()
|
||||
callback();
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
async reload (opt: { focus?: string } = {}) {
|
||||
if (this.isButton) return
|
||||
async reload(opt: { focus?: string } = {}) {
|
||||
if (this.isButton) return;
|
||||
|
||||
let url: string
|
||||
let url: string;
|
||||
if (this.subevent) {
|
||||
url = `${this.targetUrl}${this.subevent}/widget/product_list?lang=${LANG}`
|
||||
url = `${this.targetUrl}${this.subevent}/widget/product_list?lang=${LANG}`;
|
||||
} else {
|
||||
url = `${this.targetUrl}widget/product_list?lang=${LANG}`
|
||||
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)}`
|
||||
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)}`;
|
||||
|
||||
if (this.cartId) url += `&cart_id=${encodeURIComponent(this.cartId)}`
|
||||
if (this.cartId) url += `&cart_id=${encodeURIComponent(this.cartId)}`;
|
||||
if (this.date !== null) {
|
||||
url += `&date=${this.date.substring(0, 7)}`
|
||||
url += `&date=${this.date.substring(0, 7)}`;
|
||||
} else if (this.week !== null) {
|
||||
url += `&date=${this.week[0]}-W${this.week[1]}`
|
||||
url += `&date=${this.week[0]}-W${this.week[1]}`;
|
||||
}
|
||||
if (this.style !== null) url += `&style=${encodeURIComponent(this.style)}`
|
||||
if (this.style !== null)
|
||||
url += `&style=${encodeURIComponent(this.style)}`;
|
||||
|
||||
try {
|
||||
const { data, responseUrl } = await fetchProductList(url)
|
||||
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)
|
||||
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
|
||||
let adjustedUrl = newUrl;
|
||||
if (this.subevent) {
|
||||
adjustedUrl = adjustedUrl.substring(0, adjustedUrl.lastIndexOf('/', adjustedUrl.length - 1) + 1)
|
||||
adjustedUrl = adjustedUrl.substring(
|
||||
0,
|
||||
adjustedUrl.lastIndexOf("/", adjustedUrl.length - 1) + 1,
|
||||
);
|
||||
}
|
||||
this.targetUrl = adjustedUrl
|
||||
this.reload()
|
||||
return
|
||||
this.targetUrl = adjustedUrl;
|
||||
this.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
this.connectionError = false
|
||||
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 ?? []
|
||||
this.emptyText = data.empty_text ?? null
|
||||
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 ?? [];
|
||||
this.emptyText = data.empty_text ?? null;
|
||||
} 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 ?? []
|
||||
this.emptyText = data.empty_text ?? null
|
||||
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 ?? [];
|
||||
this.emptyText = data.empty_text ?? null;
|
||||
} 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 ?? []
|
||||
this.emptyText = data.empty_text ?? null
|
||||
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 ?? [];
|
||||
this.emptyText = data.empty_text ?? null;
|
||||
} 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.currency_places = data.currency_places ?? 2
|
||||
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.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.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.currency_places = data.currency_places ?? 2;
|
||||
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.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 ?? ''
|
||||
this.poweredby = data.poweredby ?? "";
|
||||
|
||||
if (this.loading > 0) {
|
||||
this.loading--
|
||||
this.triggerLoadCallback()
|
||||
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.parentStack.length > 0 &&
|
||||
this.hasSeatingPlan &&
|
||||
this.categories.length === 0 &&
|
||||
!this.frameDismissed &&
|
||||
this.useIframe &&
|
||||
!this.error &&
|
||||
!this.hasSeatingPlanWaitinglist
|
||||
) {
|
||||
this.startseating()
|
||||
this.startseating();
|
||||
} else if (opt.focus) {
|
||||
nextTick(() => {
|
||||
document.querySelector<HTMLElement>(opt.focus!)?.focus()
|
||||
})
|
||||
document.querySelector<HTMLElement>(opt.focus!)?.focus();
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
this.categories = []
|
||||
this.currency = ''
|
||||
this.currency_places = 2
|
||||
this.categories = [];
|
||||
this.currency = "";
|
||||
this.currency_places = 2;
|
||||
if (e instanceof ApiError && e.status === 429) {
|
||||
this.error = STRINGS.loading_error_429
|
||||
this.error = STRINGS.loading_error_429;
|
||||
} else {
|
||||
this.error = STRINGS.loading_error
|
||||
this.error = STRINGS.loading_error;
|
||||
}
|
||||
this.connectionError = true
|
||||
this.connectionError = true;
|
||||
if (this.loading > 0) {
|
||||
this.loading--
|
||||
this.triggerLoadCallback()
|
||||
this.loading--;
|
||||
this.triggerLoadCallback();
|
||||
}
|
||||
throw e
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
getVoucherFormTarget (): string {
|
||||
let formTarget = `${this.targetUrl}w/${globalWidgetId}/redeem?iframe=1&locale=${LANG}`
|
||||
getVoucherFormTarget(): string {
|
||||
let formTarget = `${this.targetUrl}w/${globalWidgetId}/redeem?iframe=1&locale=${LANG}`;
|
||||
if (this.cartId) {
|
||||
formTarget += `&take_cart_id=${this.cartId}`
|
||||
formTarget += `&take_cart_id=${this.cartId}`;
|
||||
}
|
||||
if (this.subevent) {
|
||||
formTarget += `&subevent=${this.subevent}`
|
||||
formTarget += `&subevent=${this.subevent}`;
|
||||
}
|
||||
if (this.widgetData) {
|
||||
formTarget += `&widget_data=${encodeURIComponent(this.widgetDataJson)}`
|
||||
formTarget += `&widget_data=${encodeURIComponent(this.widgetDataJson)}`;
|
||||
}
|
||||
formTarget += this.consentParameter
|
||||
formTarget += this.consentParameter;
|
||||
if (this.additionalURLParams) {
|
||||
formTarget += `&${this.additionalURLParams}`
|
||||
formTarget += `&${this.additionalURLParams}`;
|
||||
}
|
||||
return formTarget
|
||||
return formTarget;
|
||||
},
|
||||
handleCartResponse (data: CartResponse) {
|
||||
handleCartResponse(data: CartResponse) {
|
||||
if (data.redirect) {
|
||||
if (data.cart_id) {
|
||||
this.setCartId(data.cart_id)
|
||||
this.setCartId(data.cart_id);
|
||||
}
|
||||
|
||||
let url = data.redirect
|
||||
if (url.substring(0, 1) === '/') {
|
||||
url = `${this.targetUrl.replace(/^([^/]+:\/\/[^/]+)\/.*$/, '$1')}${url}`
|
||||
let url = data.redirect;
|
||||
if (url.substring(0, 1) === "/") {
|
||||
url = `${this.targetUrl.replace(/^([^/]+:\/\/[^/]+)\/.*$/, "$1")}${url}`;
|
||||
}
|
||||
|
||||
if (url.includes('?')) {
|
||||
url = `${url}&iframe=1&locale=${LANG}&take_cart_id=${this.cartId}`
|
||||
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 = `${url}?iframe=1&locale=${LANG}&take_cart_id=${this.cartId}`;
|
||||
}
|
||||
url += this.consentParameter
|
||||
url += this.consentParameter;
|
||||
if (this.additionalURLParams) {
|
||||
url += `&${this.additionalURLParams}`
|
||||
url += `&${this.additionalURLParams}`;
|
||||
}
|
||||
|
||||
if (data.success === false) {
|
||||
url = url.replace(/checkout\/start/g, '')
|
||||
this.overlay.errorMessage = data.message ?? null
|
||||
url = url.replace(/checkout\/start/g, "");
|
||||
this.overlay.errorMessage = data.message ?? null;
|
||||
if (data.has_cart) {
|
||||
this.overlay.errorUrlAfter = url
|
||||
this.overlay.errorUrlAfter = url;
|
||||
}
|
||||
this.overlay.frameLoading = false
|
||||
this.overlay.frameLoading = false;
|
||||
} else {
|
||||
this.overlay.frameSrc = url
|
||||
this.overlay.frameSrc = url;
|
||||
}
|
||||
} else {
|
||||
this.asyncTaskId = data.async_id
|
||||
this.asyncTaskId = data.async_id;
|
||||
if (data.check_url) {
|
||||
this.asyncTaskCheckUrl = `${this.targetUrl.replace(/^([^/]+:\/\/[^/]+)\/.*$/, '$1')}${data.check_url}`
|
||||
this.asyncTaskCheckUrl = `${this.targetUrl.replace(/^([^/]+:\/\/[^/]+)\/.*$/, "$1")}${data.check_url}`;
|
||||
}
|
||||
this.asyncTaskTimeout = window.setTimeout(() => this.pollAsyncTask(), this.asyncTaskInterval)
|
||||
this.asyncTaskInterval = 250
|
||||
this.asyncTaskTimeout = window.setTimeout(
|
||||
() => this.pollAsyncTask(),
|
||||
this.asyncTaskInterval,
|
||||
);
|
||||
this.asyncTaskInterval = 250;
|
||||
}
|
||||
},
|
||||
async pollAsyncTask () {
|
||||
if (!this.asyncTaskCheckUrl) return
|
||||
async pollAsyncTask() {
|
||||
if (!this.asyncTaskCheckUrl) return;
|
||||
try {
|
||||
const data = await checkAsyncTask(this.asyncTaskCheckUrl)
|
||||
this.handleCartResponse(data)
|
||||
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
|
||||
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)
|
||||
this.asyncTaskTimeout = window.setTimeout(
|
||||
() => this.pollAsyncTask(),
|
||||
1000,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
async buy (formData: FormData, event?: Event) {
|
||||
if (!this.useIframe) return
|
||||
if (event) event.preventDefault()
|
||||
async buy(formData: FormData, event?: Event) {
|
||||
if (!this.useIframe) return;
|
||||
if (event) event.preventDefault();
|
||||
|
||||
if (this.isButton && this.items.length === 0) {
|
||||
if (this.voucherCode) {
|
||||
this.voucherOpen(this.voucherCode)
|
||||
this.voucherOpen(this.voucherCode);
|
||||
} else {
|
||||
this.resume()
|
||||
this.resume();
|
||||
}
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${this.formAction}&locale=${LANG}&ajax=1`
|
||||
this.overlay.frameLoading = true
|
||||
this.asyncTaskInterval = 100
|
||||
const url = `${this.formAction}&locale=${LANG}&ajax=1`;
|
||||
this.overlay.frameLoading = true;
|
||||
this.asyncTaskInterval = 100;
|
||||
|
||||
try {
|
||||
const data = await submitCart(url, formData)
|
||||
this.handleCartResponse(data)
|
||||
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
|
||||
return
|
||||
this.overlay.errorMessage = STRINGS.cart_error_429;
|
||||
this.overlay.frameLoading = false;
|
||||
this.overlay.errorUrlAfter = this.newTabTarget;
|
||||
this.overlay.errorUrlAfterNewTab = true;
|
||||
return;
|
||||
} else if (e.status === 405) {
|
||||
// Likely a redirect!
|
||||
this.targetUrl = e.responseUrl.substring(0, e.responseUrl.indexOf('/cart/add') - 18)
|
||||
this.overlay.frameLoading = false
|
||||
this.buy(formData)
|
||||
return
|
||||
this.targetUrl = e.responseUrl.substring(
|
||||
0,
|
||||
e.responseUrl.indexOf("/cart/add") - 18,
|
||||
);
|
||||
this.overlay.frameLoading = false;
|
||||
this.buy(formData);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.overlay.errorMessage = STRINGS.cart_error
|
||||
this.overlay.frameLoading = false
|
||||
this.overlay.errorMessage = STRINGS.cart_error;
|
||||
this.overlay.frameLoading = false;
|
||||
}
|
||||
},
|
||||
async createCart () {
|
||||
const url = `${this.targetUrl}w/${globalWidgetId}/cart/create?ajax=1`
|
||||
async createCart() {
|
||||
const url = `${this.targetUrl}w/${globalWidgetId}/cart/create?ajax=1`;
|
||||
|
||||
try {
|
||||
this.overlay.frameLoading = true
|
||||
const data = await createCart(url)
|
||||
this.setCartId(data.cart_id)
|
||||
return true
|
||||
this.overlay.frameLoading = true;
|
||||
const data = await createCart(url);
|
||||
this.setCartId(data.cart_id);
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError && 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 instanceof ApiError && (e.status === 200 || (e.status >= 400 && e.status < 500))) {
|
||||
this.overlay.errorMessage = STRINGS.cart_error
|
||||
this.overlay.frameLoading = false
|
||||
this.overlay.errorMessage = STRINGS.cart_error_429;
|
||||
this.overlay.frameLoading = false;
|
||||
this.overlay.errorUrlAfter = this.newTabTarget;
|
||||
this.overlay.errorUrlAfterNewTab = true;
|
||||
} else if (
|
||||
e instanceof ApiError &&
|
||||
(e.status === 200 || (e.status >= 400 && e.status < 500))
|
||||
) {
|
||||
this.overlay.errorMessage = STRINGS.cart_error;
|
||||
this.overlay.frameLoading = false;
|
||||
}
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
},
|
||||
redeem (voucherCode: string, event?: Event) {
|
||||
if (!this.useIframe) return
|
||||
if (event) event.preventDefault()
|
||||
this.voucherOpen(voucherCode)
|
||||
redeem(voucherCode: string, event?: Event) {
|
||||
if (!this.useIframe) return;
|
||||
if (event) event.preventDefault();
|
||||
this.voucherOpen(voucherCode);
|
||||
},
|
||||
voucherOpen (voucherCode: string) {
|
||||
voucherOpen(voucherCode: string) {
|
||||
// TODO just use https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
|
||||
const redirectUrl = `${this.getVoucherFormTarget()}&voucher=${encodeURIComponent(voucherCode)}`
|
||||
const redirectUrl = `${this.getVoucherFormTarget()}&voucher=${encodeURIComponent(voucherCode)}`;
|
||||
if (this.useIframe) {
|
||||
this.overlay.frameSrc = redirectUrl
|
||||
this.overlay.frameSrc = redirectUrl;
|
||||
} else {
|
||||
window.open(redirectUrl)
|
||||
window.open(redirectUrl);
|
||||
}
|
||||
},
|
||||
async resume () {
|
||||
async resume() {
|
||||
if (!this.cartId && this.keepCart) {
|
||||
// create an empty cart whose id we can persist
|
||||
if (!await this.createCart()) return
|
||||
if (!(await this.createCart())) return;
|
||||
}
|
||||
let redirectUrl = `${this.targetUrl}w/${globalWidgetId}/`
|
||||
let redirectUrl = `${this.targetUrl}w/${globalWidgetId}/`;
|
||||
if (this.subevent && this.isButton && this.items.length === 0) {
|
||||
// button with subevent but no items
|
||||
redirectUrl += `${this.subevent}/`
|
||||
redirectUrl += `${this.subevent}/`;
|
||||
}
|
||||
redirectUrl += `?iframe=1&locale=${LANG}`
|
||||
redirectUrl += `?iframe=1&locale=${LANG}`;
|
||||
if (this.cartId) {
|
||||
// ajax to make sure the cart-id is used, even if the cart is currently empty
|
||||
redirectUrl += `&take_cart_id=${this.cartId}&ajax=1`
|
||||
redirectUrl += `&take_cart_id=${this.cartId}&ajax=1`;
|
||||
}
|
||||
if (this.widgetData) {
|
||||
redirectUrl += `&widget_data=${encodeURIComponent(this.widgetDataJson)}`
|
||||
redirectUrl += `&widget_data=${encodeURIComponent(this.widgetDataJson)}`;
|
||||
}
|
||||
redirectUrl += this.consentParameter
|
||||
redirectUrl += this.consentParameter;
|
||||
if (this.additionalURLParams) {
|
||||
redirectUrl += `&${this.additionalURLParams}`
|
||||
redirectUrl += `&${this.additionalURLParams}`;
|
||||
}
|
||||
if (this.useIframe) {
|
||||
this.overlay.frameSrc = redirectUrl
|
||||
this.overlay.frameSrc = redirectUrl;
|
||||
} else {
|
||||
window.open(redirectUrl)
|
||||
window.open(redirectUrl);
|
||||
}
|
||||
},
|
||||
startwaiting () {
|
||||
let redirectUrl = `${this.targetUrl}w/${globalWidgetId}/waitinglist/?iframe=1&locale=${LANG}`
|
||||
startwaiting() {
|
||||
let redirectUrl = `${this.targetUrl}w/${globalWidgetId}/waitinglist/?iframe=1&locale=${LANG}`;
|
||||
if (this.subevent) {
|
||||
redirectUrl += `&subevent=${this.subevent}`
|
||||
redirectUrl += `&subevent=${this.subevent}`;
|
||||
}
|
||||
if (this.additionalURLParams) {
|
||||
redirectUrl += `&${this.additionalURLParams}`
|
||||
redirectUrl += `&${this.additionalURLParams}`;
|
||||
}
|
||||
if (this.useIframe) {
|
||||
this.overlay.frameSrc = redirectUrl
|
||||
this.overlay.frameSrc = redirectUrl;
|
||||
} else {
|
||||
window.open(redirectUrl)
|
||||
window.open(redirectUrl);
|
||||
}
|
||||
},
|
||||
startseating () {
|
||||
let redirectUrl = `${this.targetUrl}w/${globalWidgetId}`
|
||||
startseating() {
|
||||
let redirectUrl = `${this.targetUrl}w/${globalWidgetId}`;
|
||||
if (this.subevent) {
|
||||
redirectUrl += `/${this.subevent}`
|
||||
redirectUrl += `/${this.subevent}`;
|
||||
}
|
||||
redirectUrl += `/seatingframe/?iframe=1&locale=${LANG}`
|
||||
redirectUrl += `/seatingframe/?iframe=1&locale=${LANG}`;
|
||||
if (this.voucherCode) {
|
||||
redirectUrl += `&voucher=${encodeURIComponent(this.voucherCode)}`
|
||||
redirectUrl += `&voucher=${encodeURIComponent(this.voucherCode)}`;
|
||||
}
|
||||
if (this.cartId) {
|
||||
redirectUrl += `&take_cart_id=${this.cartId}`
|
||||
redirectUrl += `&take_cart_id=${this.cartId}`;
|
||||
}
|
||||
if (this.widgetData) {
|
||||
redirectUrl += `&widget_data=${encodeURIComponent(this.widgetDataJson)}`
|
||||
redirectUrl += `&widget_data=${encodeURIComponent(this.widgetDataJson)}`;
|
||||
}
|
||||
redirectUrl += this.consentParameter
|
||||
redirectUrl += this.consentParameter;
|
||||
if (this.additionalURLParams) {
|
||||
redirectUrl += `&${this.additionalURLParams}`
|
||||
redirectUrl += `&${this.additionalURLParams}`;
|
||||
}
|
||||
if (this.useIframe) {
|
||||
this.overlay.frameSrc = redirectUrl
|
||||
this.overlay.frameSrc = redirectUrl;
|
||||
} else {
|
||||
window.open(redirectUrl)
|
||||
window.open(redirectUrl);
|
||||
}
|
||||
},
|
||||
setCartId (cartId: string) {
|
||||
this._cartId = cartId
|
||||
setCookie(this.cookieName, cartId, 30)
|
||||
}
|
||||
}
|
||||
})
|
||||
setCartId(cartId: string) {
|
||||
this._cartId = cartId;
|
||||
setCookie(this.cookieName, cartId, 30);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,92 +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
|
||||
gross: string;
|
||||
net: string;
|
||||
rate: string;
|
||||
name: string;
|
||||
includes_mixed_tax_rate?: boolean;
|
||||
}
|
||||
|
||||
export interface Availability {
|
||||
color: 'green' | 'orange' | 'red'
|
||||
text?: string
|
||||
reason?: string
|
||||
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
|
||||
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
|
||||
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[]
|
||||
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
|
||||
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[]
|
||||
date: string;
|
||||
day_formatted: string;
|
||||
events: EventEntry[];
|
||||
}
|
||||
|
||||
export interface MetaFilterField {
|
||||
key: string
|
||||
label: string
|
||||
choices: [string, string][]
|
||||
key: string;
|
||||
label: string;
|
||||
choices: [string, string][];
|
||||
}
|
||||
|
||||
export interface LightboxState {
|
||||
image: string
|
||||
description: string
|
||||
loading?: boolean
|
||||
image: string;
|
||||
description: string;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export interface WidgetData {
|
||||
referer: string
|
||||
consent?: string
|
||||
[key: string]: string | undefined
|
||||
referer: string;
|
||||
consent?: string;
|
||||
[key: string]: string | undefined;
|
||||
}
|
||||
@@ -1,100 +1,106 @@
|
||||
// Utility functions for the pretix widget
|
||||
|
||||
import { getFormat } from '~/i18n'
|
||||
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};${siteIsSecure() ? 'SameSite=None;Secure;' : ''}path=/`
|
||||
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};${siteIsSecure() ? "SameSite=None;Secure;" : ""}path=/`;
|
||||
}
|
||||
|
||||
export function getCookie (name: string): string | null {
|
||||
const value = `; ${document.cookie}`
|
||||
const parts = value.split(`; ${name}=`)
|
||||
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 parts.pop()?.split(";").shift() || null;
|
||||
}
|
||||
return 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
|
||||
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)
|
||||
export function floatformat(val: number | string, places = 2): string {
|
||||
if (typeof val === "string") {
|
||||
val = parseFloat(val);
|
||||
}
|
||||
const parts = roundTo(val, places).toFixed(places).split('.')
|
||||
const parts = roundTo(val, places).toFixed(places).split(".");
|
||||
if (places === 0) {
|
||||
return parts[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
|
||||
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]}`
|
||||
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
|
||||
export function autofloatformat(val: number | string, places = 2): string {
|
||||
const numVal = typeof val === "string" ? parseFloat(val) : val;
|
||||
if (numVal === roundTo(numVal, 0)) {
|
||||
places = 0
|
||||
places = 0;
|
||||
}
|
||||
return floatformat(numVal, places)
|
||||
return floatformat(numVal, places);
|
||||
}
|
||||
|
||||
// String/number utilities
|
||||
export function padNumber (number: number, size = 2): string {
|
||||
let s = String(number)
|
||||
export function padNumber(number: number, size = 2): string {
|
||||
let s = String(number);
|
||||
while (s.length < size) {
|
||||
s = `0${s}`
|
||||
s = `0${s}`;
|
||||
}
|
||||
return 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
|
||||
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
|
||||
return d.getDay() === 4 || (isLeap && d.getDay() === 3) ? 53 : 52;
|
||||
}
|
||||
|
||||
export function makeid (length: number): string {
|
||||
let text = ''
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||
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))
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text
|
||||
return text;
|
||||
}
|
||||
|
||||
export function siteIsSecure (): boolean {
|
||||
return /https.*/.test(document.location.protocol)
|
||||
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 || ''
|
||||
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()
|
||||
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 })
|
||||
})
|
||||
return new Promise((resolve) => {
|
||||
document.addEventListener("DOMContentLoaded", () => resolve(), {
|
||||
once: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
declare const LANG: string
|
||||
declare const LANG: string;
|
||||
@@ -1,24 +1,29 @@
|
||||
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'
|
||||
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
|
||||
export function createWidgetInstance(element: Element, htmlId?: string): App {
|
||||
let targetUrl = element.attributes.event.value;
|
||||
if (!targetUrl.match(/\/$/)) {
|
||||
targetUrl += '/'
|
||||
targetUrl += "/";
|
||||
}
|
||||
|
||||
const displayEventInfoAttr = element.attributes['display-event-info']?.value
|
||||
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 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))
|
||||
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
|
||||
widgetData[attr.name.replace(/^data-/, "")] = attr.value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,11 +31,14 @@ export function createWidgetInstance (element: Element, htmlId?: string): App {
|
||||
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,
|
||||
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,
|
||||
@@ -38,36 +46,41 @@ export function createWidgetInstance (element: Element, htmlId?: string): App {
|
||||
variations: element.attributes.variations?.value || null,
|
||||
widgetData,
|
||||
htmlId: htmlId || element.id || makeid(16),
|
||||
keepCart: true
|
||||
})
|
||||
keepCart: true,
|
||||
});
|
||||
|
||||
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
|
||||
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;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const app = createApp(WidgetComponent)
|
||||
app.provide(StoreKey, store)
|
||||
const app = createApp(WidgetComponent);
|
||||
app.provide(StoreKey, store);
|
||||
app.config.errorHandler = (error, _vm, info) => {
|
||||
console.error('[pretix-widget]', info, error)
|
||||
}
|
||||
console.error("[pretix-widget]", info, error);
|
||||
};
|
||||
// Instead of mounting to the element directly, replicate vue2.7 behaviour, where
|
||||
// the HTML-element gets replaced instead of vue3’s behaviour to replace innerHTML.
|
||||
// The latter can cause issues with custom CSS as HTML structure would change.
|
||||
// app.mount(element)
|
||||
// observer.observe(element, { attributes: true })
|
||||
const fragment = document.createDocumentFragment()
|
||||
app.mount(fragment)
|
||||
const fragment = document.createDocumentFragment();
|
||||
app.mount(fragment);
|
||||
for (const attr of element.attributes) {
|
||||
fragment.firstChild.setAttribute(attr.name, attr.value)
|
||||
fragment.firstChild.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
observer.observe(fragment.firstChild, { attributes: true })
|
||||
element.parentNode.replaceChild(fragment, element)
|
||||
observer.observe(fragment.firstChild, { attributes: true });
|
||||
element.parentNode.replaceChild(fragment, element);
|
||||
|
||||
return app
|
||||
return app;
|
||||
}
|
||||
@@ -1,38 +1,36 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
|
||||
export default defineConfig(({ mode }) => ({
|
||||
server: {
|
||||
port: 5180
|
||||
port: 5180,
|
||||
},
|
||||
plugins: [
|
||||
vue()
|
||||
],
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'~': import.meta.dirname + '/src',
|
||||
"~": import.meta.dirname + "/src",
|
||||
},
|
||||
},
|
||||
build: {
|
||||
minify: false, // django will do minification
|
||||
outDir: import.meta.dirname + '/../../../static.dist/vite/widget',
|
||||
outDir: import.meta.dirname + "/../../../static.dist/vite/widget",
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: import.meta.dirname + '/src/main.ts',
|
||||
main: import.meta.dirname + "/src/main.ts",
|
||||
},
|
||||
output: {
|
||||
format: 'iife',
|
||||
entryFileNames: 'widget.js',
|
||||
assetFileNames: 'widget.[ext]',
|
||||
format: "iife",
|
||||
entryFileNames: "widget.js",
|
||||
assetFileNames: "widget.[ext]",
|
||||
},
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['moment', 'jquery']
|
||||
exclude: ["moment", "jquery"],
|
||||
},
|
||||
define: {
|
||||
...(mode === 'development' && {
|
||||
LANG: JSON.stringify(process.env.PRETIX_WIDGET_LANG || 'en'),
|
||||
...(mode === "development" && {
|
||||
LANG: JSON.stringify(process.env.PRETIX_WIDGET_LANG || "en"),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
}));
|
||||
@@ -333,7 +333,6 @@ def test_order_update_transmission_validation(token_client, organizer, event, or
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {"invoice_address": {"transmission_info": {
|
||||
"transmission_it_sdi_pec": ["Enter a valid email address.", "Enter a valid email address."],
|
||||
"transmission_it_sdi_recipient_code": ["This field is required."]
|
||||
}}}
|
||||
|
||||
resp = token_client.patch(
|
||||
|
||||
@@ -696,7 +696,7 @@ def test_medium_autocreate_giftcard(token_client, organizer, team):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_medium_lookup_cross_organizer(token_client, organizer, organizer2, org2_event, medium2, giftcard2):
|
||||
def test_medium_lookup_cross_organizer(token_client, organizer, customer, organizer2, org2_event, medium2, giftcard2):
|
||||
with scopes_disabled():
|
||||
o = Order.objects.create(
|
||||
code='FOO', event=org2_event, email='dummy@dummy.test',
|
||||
@@ -708,7 +708,12 @@ def test_medium_lookup_cross_organizer(token_client, organizer, organizer2, org2
|
||||
personalized=True)
|
||||
op = o.positions.create(item=ticket, price=Decimal("14"))
|
||||
medium2.linked_orderpositions.add(op)
|
||||
medium2.customer = customer
|
||||
medium2.linked_giftcard = giftcard2
|
||||
medium2.claim_token = "1234"
|
||||
medium2.info = {"foo": "bar"}
|
||||
medium2.notes = "Internal notes"
|
||||
medium2.label = "Max Muster"
|
||||
medium2.save()
|
||||
|
||||
resp = token_client.post(
|
||||
@@ -755,3 +760,9 @@ def test_medium_lookup_cross_organizer(token_client, organizer, organizer2, org2
|
||||
assert resp.data["result"]["organizer"] == "partner"
|
||||
assert resp.data["result"]["linked_giftcard"] is not None
|
||||
assert resp.data["result"]["linked_orderposition"] is None
|
||||
assert resp.data["result"]["linked_orderpositions"] == []
|
||||
assert resp.data["result"]["customer"] is None
|
||||
assert resp.data["result"]["info"] == {}
|
||||
assert resp.data["result"]["notes"] is None
|
||||
assert resp.data["result"]["claim_token"] is None
|
||||
assert resp.data["result"]["label"] is None
|
||||
+6
-4
@@ -3,11 +3,13 @@
|
||||
"src/pretix/static/**/*",
|
||||
"src/pretix/static/**/*.vue",
|
||||
"src/pretix/plugins/webcheckin/**/*",
|
||||
"src/pretix/plugins/webcheckin/**/*.vue"
|
||||
"src/pretix/plugins/webcheckin/**/*.vue",
|
||||
],
|
||||
"exclude": ["**/*.js"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {},
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
},
|
||||
"strict": false,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
@@ -18,7 +20,7 @@
|
||||
"noErrorTruncation": true,
|
||||
"noImplicitThis": true,
|
||||
"isolatedModules": true,
|
||||
"types": ["node", "events"]
|
||||
"noEmit": true,
|
||||
},
|
||||
"vueCompilerOptions": {
|
||||
"plugins": ["@vue/language-plugin-pug"]
|
||||
|
||||
Reference in new issue
Block a user