tixbit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,267 @@
1
+ /** Configuration for the TixBit client. */
2
+ interface TixBitConfig {
3
+ /**
4
+ * Base URL of the TixBit web app.
5
+ * @default "https://tixbit.com"
6
+ */
7
+ baseUrl?: string;
8
+ /**
9
+ * Optional request timeout in milliseconds.
10
+ * @default 15000
11
+ */
12
+ timeoutMs?: number;
13
+ /**
14
+ * Optional API key for authenticated endpoints (future use).
15
+ */
16
+ apiKey?: string;
17
+ }
18
+ interface SearchEventsParams {
19
+ /** Free-text search query (e.g. "Hawks", "Taylor Swift"). */
20
+ query?: string;
21
+ /** City name (e.g. "Atlanta"). */
22
+ city?: string;
23
+ /** Two-letter US state code (e.g. "GA"). */
24
+ state?: string;
25
+ /** Category slug (e.g. "nba-basketball", "mlb-baseball"). */
26
+ category?: string;
27
+ /** ISO date string — only events on or after this date. */
28
+ startDate?: string;
29
+ /** ISO date string — only events on or before this date. */
30
+ endDate?: string;
31
+ /** Page number (1-indexed). @default 1 */
32
+ page?: number;
33
+ /** Page size. @default 25 */
34
+ size?: number;
35
+ }
36
+ interface TixBitEvent {
37
+ id: string;
38
+ external_event_id: string;
39
+ slug: string;
40
+ name: string;
41
+ date: string;
42
+ venue_name: string | null;
43
+ venue_city: string | null;
44
+ venue_state: string | null;
45
+ image_url: string | null;
46
+ category_name: string | null;
47
+ category_event_type: string | null;
48
+ has_listings: boolean;
49
+ inventory: {
50
+ total_available: number;
51
+ min_price: number;
52
+ max_price: number;
53
+ };
54
+ }
55
+ interface SearchEventsResult {
56
+ events: TixBitEvent[];
57
+ pagination: {
58
+ page: number;
59
+ size: number;
60
+ total: number;
61
+ totalPages: number;
62
+ hasNext: boolean;
63
+ hasPrev: boolean;
64
+ };
65
+ }
66
+ interface GetListingsParams {
67
+ /** External event ID (from search results). */
68
+ eventId: string;
69
+ /** Page size. @default 50 */
70
+ size?: number;
71
+ /** Page number. @default 1 */
72
+ page?: number;
73
+ /** Sort direction for price. @default "asc" */
74
+ orderByDirection?: "asc" | "desc";
75
+ }
76
+ interface TixBitListing {
77
+ id: string;
78
+ price: number;
79
+ quantity: number;
80
+ quantities_list: number[];
81
+ section: string | null;
82
+ row: string | null;
83
+ seat_numbers: string | null;
84
+ listing_hash: string;
85
+ notes: string | null;
86
+ delivery_method: string | null;
87
+ splits: number[];
88
+ raw: Record<string, unknown>;
89
+ }
90
+ interface GetListingsResult {
91
+ listings: TixBitListing[];
92
+ meta: {
93
+ total: number;
94
+ page: number;
95
+ size: number;
96
+ cacheSource?: string;
97
+ };
98
+ }
99
+ interface CheckoutParams {
100
+ /** Listing ID to purchase (from getListings results). */
101
+ listingId: string;
102
+ /** Number of tickets to buy. */
103
+ quantity: number;
104
+ }
105
+ /** A checkout link that the user opens in a browser to complete purchase. */
106
+ interface CheckoutLink {
107
+ /** Full URL to the checkout page on tixbit.com. */
108
+ url: string;
109
+ /** Listing ID being purchased. */
110
+ listingId: string;
111
+ /** Ticket quantity. */
112
+ quantity: number;
113
+ }
114
+ interface BrowseEventsParams {
115
+ /** User latitude for location-aware results. */
116
+ latitude?: number;
117
+ /** User longitude for location-aware results. */
118
+ longitude?: number;
119
+ /** Preferred city. */
120
+ city?: string;
121
+ /** Preferred state. */
122
+ state?: string;
123
+ /** Number of results. @default 18 */
124
+ size?: number;
125
+ /** Category event type: SPORT, CONCERT, THEATER. */
126
+ categoryEventType?: "SPORT" | "CONCERT" | "THEATER" | "ALL";
127
+ }
128
+ interface BrowseEventsResult {
129
+ events: TixBitEvent[];
130
+ total: number;
131
+ }
132
+ interface GetSeatmapParams {
133
+ /** External event ID (from search results). */
134
+ eventId: string;
135
+ }
136
+ /** A single section in the venue's seating chart. */
137
+ interface SeatmapSection {
138
+ /** Section ID (e.g. "80841"). */
139
+ id: string;
140
+ /** Human-readable section name (e.g. "101", "FLOOR3", "201"). */
141
+ name: string;
142
+ /**
143
+ * Center coordinates of the section label on the map.
144
+ * Useful for understanding relative position.
145
+ */
146
+ x: number;
147
+ y: number;
148
+ }
149
+ /** A zone grouping sections (e.g. "Section", "Floor", "Suite"). */
150
+ interface SeatmapZone {
151
+ id: string;
152
+ name: string;
153
+ sections: SeatmapSection[];
154
+ }
155
+ /** Venue metadata returned with the seatmap. */
156
+ interface SeatmapVenue {
157
+ name: string;
158
+ address?: string;
159
+ city: string;
160
+ region: string;
161
+ country: string;
162
+ time_zone: string;
163
+ }
164
+ /** Result from the seating chart API. */
165
+ interface SeatmapResult {
166
+ success: boolean;
167
+ event_id: string;
168
+ venue_id: string;
169
+ venue_name: string;
170
+ configuration_id: string;
171
+ configuration_name: string;
172
+ /** URL to the background SVG image of the venue map. */
173
+ background_image: string | null;
174
+ /** URL to the coordinates JSON (section polygons). */
175
+ coordinates_url: string | null;
176
+ has_coordinates: boolean;
177
+ capacity: number | null;
178
+ venue: SeatmapVenue;
179
+ /** Parsed section data from coordinates (when available). */
180
+ zones: SeatmapZone[];
181
+ /** All section names for quick reference. */
182
+ section_names: string[];
183
+ }
184
+
185
+ declare class TixBitClient {
186
+ private readonly baseUrl;
187
+ private readonly timeoutMs;
188
+ private readonly apiKey;
189
+ constructor(config?: TixBitConfig);
190
+ private request;
191
+ private qs;
192
+ /**
193
+ * Search for events by keyword, city, state, category, or date range.
194
+ *
195
+ * @example
196
+ * ```ts
197
+ * const results = await client.searchEvents({ query: "Hawks", state: "GA" });
198
+ * ```
199
+ */
200
+ searchEvents(params?: SearchEventsParams): Promise<SearchEventsResult>;
201
+ /**
202
+ * Browse upcoming events near a location (homepage-style).
203
+ *
204
+ * @example
205
+ * ```ts
206
+ * const nearby = await client.browse({ city: "Atlanta", state: "GA" });
207
+ * ```
208
+ */
209
+ browse(params?: BrowseEventsParams): Promise<BrowseEventsResult>;
210
+ /**
211
+ * Get available ticket listings for an event.
212
+ *
213
+ * @example
214
+ * ```ts
215
+ * const listings = await client.getListings({ eventId: "abc123" });
216
+ * ```
217
+ */
218
+ getListings(params: GetListingsParams): Promise<GetListingsResult>;
219
+ /**
220
+ * Create a checkout link for a listing.
221
+ *
222
+ * Returns a URL to the TixBit checkout page where the user can
223
+ * sign in and complete their purchase (card, crypto, etc.).
224
+ *
225
+ * @example
226
+ * ```ts
227
+ * const link = client.createCheckoutLink({
228
+ * listingId: "P2JO5OBX",
229
+ * quantity: 2,
230
+ * });
231
+ *
232
+ * console.log(link.url);
233
+ * // → "https://tixbit.com/checkout/process?listing=P2JO5OBX&quantity=2"
234
+ * ```
235
+ */
236
+ createCheckoutLink(params: CheckoutParams): CheckoutLink;
237
+ /**
238
+ * Build the direct URL to an event page on tixbit.com.
239
+ */
240
+ eventUrl(slugOrId: string): string;
241
+ /**
242
+ * Get the seating chart for an event's venue.
243
+ *
244
+ * Returns section-level data including section names, positions, and
245
+ * the venue background image URL. Use this to help users understand
246
+ * where their tickets are located.
247
+ *
248
+ * @example
249
+ * ```ts
250
+ * const seatmap = await client.getSeatmap({ eventId: "4BKJMDZ" });
251
+ * console.log(seatmap.venue_name); // "State Farm Arena"
252
+ * console.log(seatmap.section_names); // ["101", "102", ...]
253
+ * ```
254
+ */
255
+ getSeatmap(params: GetSeatmapParams): Promise<SeatmapResult>;
256
+ /**
257
+ * Fetch the coordinates JSON and parse it into zones/sections.
258
+ */
259
+ private fetchAndParseCoordinates;
260
+ }
261
+ declare class TixBitApiError extends Error {
262
+ readonly status: number;
263
+ readonly url: string;
264
+ constructor(message: string, status: number, url: string);
265
+ }
266
+
267
+ export { type BrowseEventsParams, type BrowseEventsResult, type CheckoutLink, type CheckoutParams, type GetListingsParams, type GetListingsResult, type GetSeatmapParams, type SearchEventsParams, type SearchEventsResult, type SeatmapResult, type SeatmapSection, type SeatmapVenue, type SeatmapZone, TixBitApiError, TixBitClient, type TixBitConfig, type TixBitEvent, type TixBitListing };
package/dist/index.js ADDED
@@ -0,0 +1,317 @@
1
+ // src/client.ts
2
+ var DEFAULT_BASE_URL = "https://tixbit.com";
3
+ var DEFAULT_TIMEOUT_MS = 15e3;
4
+ var USER_AGENT = "@tixbit/sdk";
5
+ var TixBitClient = class {
6
+ baseUrl;
7
+ timeoutMs;
8
+ apiKey;
9
+ constructor(config = {}) {
10
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
11
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
12
+ this.apiKey = config.apiKey;
13
+ }
14
+ // ── HTTP helpers ──────────────────────────────────────────────────────────
15
+ async request(path, init) {
16
+ const url = `${this.baseUrl}${path}`;
17
+ const controller = new AbortController();
18
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
19
+ const headers = {
20
+ Accept: "application/json",
21
+ "User-Agent": USER_AGENT,
22
+ ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
23
+ };
24
+ try {
25
+ const res = await fetch(url, {
26
+ ...init,
27
+ headers: { ...headers, ...init?.headers },
28
+ signal: controller.signal
29
+ });
30
+ if (!res.ok) {
31
+ const text = await res.text().catch(() => "");
32
+ throw new TixBitApiError(
33
+ `${res.status} ${res.statusText}: ${text.slice(0, 300)}`,
34
+ res.status,
35
+ url
36
+ );
37
+ }
38
+ return await res.json();
39
+ } finally {
40
+ clearTimeout(timer);
41
+ }
42
+ }
43
+ qs(params) {
44
+ const entries = Object.entries(params).filter(([, v]) => v !== void 0 && v !== null && v !== "").map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
45
+ return entries.length ? `?${entries.join("&")}` : "";
46
+ }
47
+ // ── Search Events ─────────────────────────────────────────────────────────
48
+ /**
49
+ * Search for events by keyword, city, state, category, or date range.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * const results = await client.searchEvents({ query: "Hawks", state: "GA" });
54
+ * ```
55
+ */
56
+ async searchEvents(params = {}) {
57
+ const query = this.qs({
58
+ q: params.query,
59
+ city: params.city,
60
+ state: params.state,
61
+ category: params.category,
62
+ startDate: params.startDate,
63
+ endDate: params.endDate,
64
+ page: params.page,
65
+ size: params.size ?? 25
66
+ });
67
+ const data = await this.request(`/api/events/search${query}`);
68
+ return {
69
+ events: (data.events ?? []).map(normalizeEvent),
70
+ pagination: data.pagination
71
+ };
72
+ }
73
+ // ── Browse (Location-Aware) ───────────────────────────────────────────────
74
+ /**
75
+ * Browse upcoming events near a location (homepage-style).
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * const nearby = await client.browse({ city: "Atlanta", state: "GA" });
80
+ * ```
81
+ */
82
+ async browse(params = {}) {
83
+ const query = this.qs({
84
+ size: params.size ?? 18,
85
+ context: "homepage",
86
+ recommendation: "upcoming",
87
+ nearLat: params.latitude,
88
+ nearLng: params.longitude,
89
+ preferCity: params.city,
90
+ preferState: params.state,
91
+ categoryEventType: params.categoryEventType
92
+ });
93
+ const data = await this.request(`/api/events${query}`);
94
+ return {
95
+ events: (data.events ?? []).map(normalizeEvent),
96
+ total: data.total
97
+ };
98
+ }
99
+ // ── Listings ──────────────────────────────────────────────────────────────
100
+ /**
101
+ * Get available ticket listings for an event.
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * const listings = await client.getListings({ eventId: "abc123" });
106
+ * ```
107
+ */
108
+ async getListings(params) {
109
+ const query = this.qs({
110
+ size: params.size ?? 50,
111
+ page: params.page ?? 1,
112
+ order_by_direction: params.orderByDirection ?? "asc"
113
+ });
114
+ const data = await this.request(`/api/events/${encodeURIComponent(params.eventId)}/listings${query}`);
115
+ const listings = (data.data ?? []).map(normalizeListing);
116
+ return {
117
+ listings,
118
+ meta: {
119
+ total: data.meta?.total ?? listings.length,
120
+ page: data.meta?.page ?? params.page ?? 1,
121
+ size: data.meta?.size ?? params.size ?? 50,
122
+ cacheSource: data.meta?.cacheSource
123
+ }
124
+ };
125
+ }
126
+ // ── Checkout Link ──────────────────────────────────────────────────────────
127
+ /**
128
+ * Create a checkout link for a listing.
129
+ *
130
+ * Returns a URL to the TixBit checkout page where the user can
131
+ * sign in and complete their purchase (card, crypto, etc.).
132
+ *
133
+ * @example
134
+ * ```ts
135
+ * const link = client.createCheckoutLink({
136
+ * listingId: "P2JO5OBX",
137
+ * quantity: 2,
138
+ * });
139
+ *
140
+ * console.log(link.url);
141
+ * // → "https://tixbit.com/checkout/process?listing=P2JO5OBX&quantity=2"
142
+ * ```
143
+ */
144
+ createCheckoutLink(params) {
145
+ const quantity = Math.max(1, Math.min(8, Math.round(params.quantity)));
146
+ const url = `${this.baseUrl}/checkout/process?listing=${encodeURIComponent(params.listingId)}&quantity=${quantity}`;
147
+ return {
148
+ url,
149
+ listingId: params.listingId,
150
+ quantity
151
+ };
152
+ }
153
+ // ── Event URL helper ──────────────────────────────────────────────────────
154
+ /**
155
+ * Build the direct URL to an event page on tixbit.com.
156
+ */
157
+ eventUrl(slugOrId) {
158
+ return `${this.baseUrl}/events/${slugOrId}`;
159
+ }
160
+ // ── Seatmap ───────────────────────────────────────────────────────────────
161
+ /**
162
+ * Get the seating chart for an event's venue.
163
+ *
164
+ * Returns section-level data including section names, positions, and
165
+ * the venue background image URL. Use this to help users understand
166
+ * where their tickets are located.
167
+ *
168
+ * @example
169
+ * ```ts
170
+ * const seatmap = await client.getSeatmap({ eventId: "4BKJMDZ" });
171
+ * console.log(seatmap.venue_name); // "State Farm Arena"
172
+ * console.log(seatmap.section_names); // ["101", "102", ...]
173
+ * ```
174
+ */
175
+ async getSeatmap(params) {
176
+ const data = await this.request(`/api/events/${encodeURIComponent(params.eventId)}/seating-chart`);
177
+ let zones = [];
178
+ let sectionNames = [];
179
+ if (data.has_coordinates && data.coordinates) {
180
+ try {
181
+ zones = await this.fetchAndParseCoordinates(data.coordinates);
182
+ sectionNames = zones.flatMap(
183
+ (z) => z.sections.map((s) => s.name)
184
+ );
185
+ } catch {
186
+ }
187
+ }
188
+ return {
189
+ success: data.success,
190
+ event_id: data.event_id,
191
+ venue_id: data.venue_id,
192
+ venue_name: data.venue_name,
193
+ configuration_id: data.configuration_id,
194
+ configuration_name: data.configuration_name,
195
+ background_image: data.background_image ?? null,
196
+ coordinates_url: data.coordinates ?? null,
197
+ has_coordinates: data.has_coordinates,
198
+ capacity: data.capacity ?? null,
199
+ venue: data.venue_data,
200
+ zones,
201
+ section_names: sectionNames
202
+ };
203
+ }
204
+ /**
205
+ * Fetch the coordinates JSON and parse it into zones/sections.
206
+ */
207
+ async fetchAndParseCoordinates(coordinatesUrl) {
208
+ const fullUrl = coordinatesUrl.startsWith("http") ? coordinatesUrl : `${this.baseUrl}${coordinatesUrl}`;
209
+ const controller = new AbortController();
210
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
211
+ try {
212
+ const res = await fetch(fullUrl, {
213
+ headers: {
214
+ Accept: "application/json",
215
+ "User-Agent": USER_AGENT
216
+ },
217
+ signal: controller.signal
218
+ });
219
+ if (!res.ok) return [];
220
+ const coords = await res.json();
221
+ if (!coords.zones) return [];
222
+ return coords.zones.map((zone) => ({
223
+ id: zone.id,
224
+ name: zone.name,
225
+ sections: (zone.sections ?? []).map((section) => {
226
+ const label = section.labels?.[0];
227
+ return {
228
+ id: section.id,
229
+ name: section.name,
230
+ x: label?.x ?? 0,
231
+ y: label?.y ?? 0
232
+ };
233
+ })
234
+ }));
235
+ } finally {
236
+ clearTimeout(timer);
237
+ }
238
+ }
239
+ };
240
+ function normalizeEvent(raw) {
241
+ const e = raw;
242
+ return {
243
+ id: str(e.id) ?? str(e.external_event_id) ?? "",
244
+ external_event_id: str(e.external_event_id) ?? str(e.externalEventId) ?? str(e.id) ?? "",
245
+ slug: str(e.slug) ?? str(e.external_event_id) ?? "",
246
+ name: str(e.name) ?? str(e.performer) ?? "",
247
+ date: resolveDate(e),
248
+ venue_name: str(e.venue_name) ?? str(e.venueName) ?? null,
249
+ venue_city: str(e.venue_city) ?? str(e.venueCity) ?? null,
250
+ venue_state: str(e.venue_state) ?? str(e.venueState) ?? null,
251
+ image_url: str(e.image_url) ?? str(e.imageUrl) ?? null,
252
+ category_name: str(e.category_name) ?? str(e.categoryName) ?? null,
253
+ category_event_type: str(e.category_event_type) ?? str(e.categoryEventType) ?? null,
254
+ has_listings: Boolean(e.has_listings),
255
+ inventory: normalizeInventory(e.inventory)
256
+ };
257
+ }
258
+ function normalizeInventory(raw) {
259
+ if (!raw || typeof raw !== "object") {
260
+ return { total_available: 0, min_price: 0, max_price: 0 };
261
+ }
262
+ const inv = raw;
263
+ return {
264
+ total_available: num(inv.total_available) ?? 0,
265
+ min_price: num(inv.min_price) ?? 0,
266
+ max_price: num(inv.max_price) ?? 0
267
+ };
268
+ }
269
+ function normalizeListing(raw) {
270
+ const outer = raw;
271
+ const attrs = outer.attributes ?? outer;
272
+ return {
273
+ id: str(outer.id) ?? str(attrs.id) ?? "",
274
+ // price_per_ticket is the fee-inclusive per-ticket price (in dollars, not cents)
275
+ price: num(attrs.price_per_ticket) ?? num(attrs.price) ?? 0,
276
+ quantity: num(attrs.quantity) ?? num(attrs.available_quantity) ?? 0,
277
+ quantities_list: Array.isArray(attrs.quantities_list) ? attrs.quantities_list : [],
278
+ section: str(attrs.section) ?? null,
279
+ row: str(attrs.row) ?? null,
280
+ seat_numbers: str(attrs.seat_numbers) ?? null,
281
+ listing_hash: str(attrs.listing_hash) ?? "",
282
+ notes: str(attrs.notes) ?? null,
283
+ delivery_method: str(attrs.delivery_method) ?? str(attrs.delivery_type) ?? null,
284
+ splits: Array.isArray(attrs.splits) ? attrs.splits : [],
285
+ raw: outer
286
+ };
287
+ }
288
+ function resolveDate(e) {
289
+ if (typeof e.date === "string") return e.date;
290
+ if (typeof e.date === "number") return new Date(e.date).toISOString();
291
+ if (e.date && typeof e.date === "object") {
292
+ const d = e.date;
293
+ if (d.month && d.day && d.year) {
294
+ return `${d.month} ${d.day}, ${d.year}`;
295
+ }
296
+ }
297
+ if (typeof e.date_ms === "number") return new Date(e.date_ms).toISOString();
298
+ return "";
299
+ }
300
+ function str(v) {
301
+ return typeof v === "string" && v.length > 0 ? v : null;
302
+ }
303
+ function num(v) {
304
+ return typeof v === "number" && Number.isFinite(v) ? v : null;
305
+ }
306
+ var TixBitApiError = class extends Error {
307
+ constructor(message, status, url) {
308
+ super(message);
309
+ this.status = status;
310
+ this.url = url;
311
+ this.name = "TixBitApiError";
312
+ }
313
+ };
314
+ export {
315
+ TixBitApiError,
316
+ TixBitClient
317
+ };
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "tixbit",
3
+ "version": "0.1.0",
4
+ "description": "Search events, view seatmaps, browse listings, and buy tickets on TixBit",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "tixbit": "dist/cli.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist/*.js",
19
+ "dist/*.d.ts",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsup",
25
+ "dev": "tsup --watch",
26
+ "typecheck": "tsc --noEmit",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "prepublishOnly": "npm run build"
30
+ },
31
+ "dependencies": {
32
+ "commander": "^13.1.0"
33
+ },
34
+ "devDependencies": {
35
+ "tsup": "^8.4.0",
36
+ "typescript": "^5.7.0",
37
+ "vitest": "^3.1.0"
38
+ },
39
+ "engines": {
40
+ "node": ">=20"
41
+ },
42
+ "keywords": [
43
+ "tixbit",
44
+ "tickets",
45
+ "events",
46
+ "seatmap",
47
+ "seating-chart",
48
+ "cli",
49
+ "sdk",
50
+ "concerts",
51
+ "sports",
52
+ "nba",
53
+ "mlb",
54
+ "nfl"
55
+ ],
56
+ "homepage": "https://tixbit.com",
57
+ "license": "MIT",
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "git+https://github.com/tixbit/sdk.git"
61
+ },
62
+ "bugs": {
63
+ "url": "https://github.com/tixbit/sdk/issues"
64
+ },
65
+ "publishConfig": {
66
+ "access": "public"
67
+ }
68
+ }