@scalemule/news-publication 0.0.52 → 0.0.55

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.
@@ -1,9 +1,9 @@
1
1
  "use client";
2
- import { PawMark } from './chunk-N4JMMFPK.js';
2
+ import { PawMark } from './chunk-ZALGE4PF.js';
3
3
  import './chunk-4JRWZXAP.js';
4
4
  import './chunk-66GI2IX6.js';
5
5
  import { PET_PHOTO_CATEGORIES, PET_CELEBRATIONS, PET_SAFETY } from './chunk-AFJ6S5WJ.js';
6
- import { useState, useEffect, useRef } from 'react';
6
+ import React, { createContext, useState, useRef, useEffect, useId, useContext } from 'react';
7
7
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
8
8
 
9
9
  function localDateTime(value) {
@@ -65,6 +65,56 @@ function PetSignIn({ returnTo = "/account" }) {
65
65
  /* @__PURE__ */ jsx("p", { className: "paw-safety", children: "Your existing network sign-in works here. Public browsing is always open." })
66
66
  ] });
67
67
  }
68
+ var FieldErrors = createContext({ errors: {}, clear: () => {
69
+ } });
70
+ function PetField({
71
+ children,
72
+ className = ""
73
+ }) {
74
+ const id = useId();
75
+ const { errors, clear } = useContext(FieldErrors);
76
+ const error = errors[id];
77
+ const nodes = React.Children.toArray(children);
78
+ const label = nodes.filter((node) => typeof node === "string").join(" ").trim();
79
+ return /* @__PURE__ */ jsxs("label", { className: `paw-field ${className}`, htmlFor: id, children: [
80
+ nodes.map((node) => {
81
+ if (!React.isValidElement(node) || !["input", "select", "textarea", "progress"].includes(
82
+ String(node.type)
83
+ ))
84
+ return node;
85
+ const original = node.props.onChange;
86
+ return React.cloneElement(node, {
87
+ id,
88
+ "data-field-label": label,
89
+ "aria-invalid": error ? true : void 0,
90
+ "aria-describedby": [node.props["aria-describedby"], error ? `${id}-error` : void 0].filter(Boolean).join(" ") || void 0,
91
+ onChange: (event) => {
92
+ clear(id);
93
+ original?.(event);
94
+ }
95
+ });
96
+ }),
97
+ error && /* @__PURE__ */ jsx("span", { className: "paw-field-error", id: `${id}-error`, children: error })
98
+ ] });
99
+ }
100
+ function fieldMessage(control, blank = false) {
101
+ const label = control.dataset.fieldLabel || "this field";
102
+ if (control.validity.valueMissing || blank) {
103
+ if (control instanceof HTMLInputElement && control.type === "checkbox")
104
+ return "Accept the publication permission to continue.";
105
+ if (control instanceof HTMLInputElement && control.type === "file")
106
+ return "Add at least one photograph of your pet.";
107
+ if (label === "Pet name") return "Add your pet\u2019s name.";
108
+ if (label === "Home community") return "Choose your home community.";
109
+ if (label === "Your story") return "Tell us a little about your pet.";
110
+ return `Add ${label.toLowerCase()} to continue.`;
111
+ }
112
+ if (control.validity.typeMismatch || control.validity.patternMismatch)
113
+ return "Enter a complete, valid link, including https://.";
114
+ if (control.validity.rangeUnderflow || control.validity.rangeOverflow)
115
+ return "Choose a value within the allowed range.";
116
+ return `Check ${label.toLowerCase()} and try again.`;
117
+ }
68
118
  function PetSubmissionForm({
69
119
  communities,
70
120
  policy,
@@ -75,6 +125,8 @@ function PetSubmissionForm({
75
125
  accountUrl = "/account",
76
126
  onSaved
77
127
  }) {
128
+ const [fieldErrors, setFieldErrors] = useState({});
129
+ const formRef = useRef(null);
78
130
  const [deviceReady, setDeviceReady] = useState(false);
79
131
  useEffect(() => setDeviceReady(true), []);
80
132
  const [kind, setKind] = useState(initial?.kind || "spotlight"), [content, setContent] = useState(initial?.content || empty);
@@ -187,496 +239,608 @@ function PetSubmissionForm({
187
239
  /* @__PURE__ */ jsx("p", { children: kind === "profile" && visibility === "private" ? "Your profile is saved privately. Only you can see it." : "Your contribution is saved for editorial review. It will appear publicly only after approval." }),
188
240
  /* @__PURE__ */ jsx("a", { href: accountUrl, children: "View your pets and submissions" })
189
241
  ] });
190
- return /* @__PURE__ */ jsxs(
191
- "form",
242
+ return /* @__PURE__ */ jsx(
243
+ FieldErrors.Provider,
192
244
  {
193
- className: "paw-form",
194
- onSubmit: (event) => {
195
- event.preventDefault();
196
- if (step === 0) {
197
- setError("");
198
- setStep(1);
199
- } else void save();
245
+ value: {
246
+ errors: fieldErrors,
247
+ clear: (id) => setFieldErrors((old) => {
248
+ const next = { ...old };
249
+ delete next[id];
250
+ return next;
251
+ })
200
252
  },
201
- children: [
202
- /* @__PURE__ */ jsx("p", { className: "paw-context", children: step === 0 ? "Your pet\u2019s story" : "Your private preview" }),
203
- /* @__PURE__ */ jsx("h1", { children: kind === "celebration" && content.category === "memorial" ? "Remember a beloved companion." : "Someone worth meeting." }),
204
- /* @__PURE__ */ jsx("p", { className: "paw-safety", children: "Choose a community, never a home address. Keep phone numbers, email addresses and microchip details out of your story." }),
205
- step === 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
206
- !initial && /* @__PURE__ */ jsxs("label", { children: [
207
- "What would you like to share?",
208
- /* @__PURE__ */ jsxs(
209
- "select",
210
- {
211
- value: kind,
212
- onChange: (e) => {
213
- const k = e.target.value;
214
- setKind(k);
215
- setContent((c) => ({
216
- ...c,
217
- source: ["event", "adoption"].includes(k) ? {
218
- organization: "",
219
- official_url: "",
220
- availability_confirmed: false
221
- } : null,
222
- event: k === "event" ? {
223
- starts_at: "",
224
- ends_at: "",
225
- event_type: "adoption",
226
- cost: "unknown",
227
- cost_verified: false
228
- } : null
229
- }));
230
- change(
231
- "category",
232
- k === "celebration" ? "birthday" : k === "photo" ? "cute" : ""
233
- );
234
- },
235
- children: [
236
- /* @__PURE__ */ jsx("option", { value: "spotlight", children: "Paw of the Week submission" }),
237
- /* @__PURE__ */ jsx("option", { value: "profile", children: "Pet profile" }),
238
- /* @__PURE__ */ jsx("option", { value: "photo", children: "Photo board" }),
239
- /* @__PURE__ */ jsx("option", { value: "celebration", children: "Pet celebration" }),
240
- /* @__PURE__ */ jsx("option", { value: "lost", children: "Lost pet report" }),
241
- /* @__PURE__ */ jsx("option", { value: "found", children: "Found pet report" }),
242
- /* @__PURE__ */ jsx("option", { value: "event", children: "Organization event" }),
243
- /* @__PURE__ */ jsx("option", { value: "adoption", children: "Shelter or rescue adoption listing" })
244
- ]
245
- }
246
- )
247
- ] }),
248
- !initial && kind !== "profile" && profiles.length > 0 && /* @__PURE__ */ jsxs("label", { children: [
249
- "Start with one of your pets",
250
- /* @__PURE__ */ jsxs(
251
- "select",
252
- {
253
- value: profileId || "",
254
- onChange: (e) => {
255
- const p = profiles.find((p2) => p2.id === e.target.value);
256
- setProfileId(p?.id || null);
257
- if (p) {
258
- setContent({
259
- ...p.content,
260
- category: content.category,
261
- source: content.source,
262
- event: content.event
263
- });
264
- setCommunity(
265
- communities.some((c) => c.id === p.area_id) ? p.area_id : defaultCommunity || ""
266
- );
267
- }
268
- },
269
- children: [
270
- /* @__PURE__ */ jsx("option", { value: "", children: "Enter pet details" }),
271
- profiles.map((p) => /* @__PURE__ */ jsx("option", { value: p.id, children: p.content.name }, p.id))
272
- ]
273
- }
274
- )
275
- ] }),
276
- /* @__PURE__ */ jsxs("label", { children: [
277
- kind === "event" ? "Event title" : "Pet name",
278
- /* @__PURE__ */ jsx(
279
- "input",
280
- {
281
- required: true,
282
- maxLength: 100,
283
- value: content.name,
284
- onChange: (e) => change("name", e.target.value),
285
- autoComplete: "off"
286
- }
287
- )
288
- ] }),
289
- /* @__PURE__ */ jsxs("label", { children: [
290
- "Species",
291
- /* @__PURE__ */ jsx(
292
- "input",
293
- {
294
- required: true,
295
- maxLength: 50,
296
- value: content.species,
297
- onChange: (e) => change("species", e.target.value)
298
- }
299
- )
300
- ] }),
301
- /* @__PURE__ */ jsxs("label", { children: [
302
- "Home community",
303
- /* @__PURE__ */ jsxs(
304
- "select",
305
- {
306
- required: true,
307
- value: community,
308
- onChange: (e) => setCommunity(e.target.value),
309
- children: [
310
- /* @__PURE__ */ jsx("option", { value: "", children: "Choose a community" }),
311
- communities.map((c) => /* @__PURE__ */ jsx("option", { value: c.id, children: c.name }, c.id))
312
- ]
253
+ children: /* @__PURE__ */ jsxs(
254
+ "form",
255
+ {
256
+ ref: formRef,
257
+ noValidate: true,
258
+ className: "paw-form paw-submission",
259
+ onSubmit: (event) => {
260
+ event.preventDefault();
261
+ const errors = {};
262
+ let first;
263
+ for (const element of Array.from(event.currentTarget.elements)) {
264
+ if (!(element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement))
265
+ continue;
266
+ const blank = element.required && element.type !== "checkbox" && element.type !== "file" && !element.value.trim();
267
+ if (element.willValidate && (!element.validity.valid || blank)) {
268
+ errors[element.id] = fieldMessage(element, blank);
269
+ first ||= element;
313
270
  }
314
- )
315
- ] }),
316
- kind === "profile" && /* @__PURE__ */ jsxs("fieldset", { children: [
317
- /* @__PURE__ */ jsx("legend", { children: "A few favorite things" }),
318
- /* @__PURE__ */ jsxs("label", { children: [
319
- "Breed or mix (optional)",
320
- /* @__PURE__ */ jsx(
321
- "input",
322
- {
323
- value: content.breed,
324
- maxLength: 100,
325
- onChange: (e) => change("breed", e.target.value)
326
- }
327
- )
328
- ] }),
329
- /* @__PURE__ */ jsxs("label", { children: [
330
- "Birthday (optional)",
331
- /* @__PURE__ */ jsx(
332
- "input",
333
- {
334
- type: "date",
335
- value: content.birthday || "",
336
- onChange: (e) => change("birthday", e.target.value || null)
337
- }
338
- )
339
- ] }),
340
- /* @__PURE__ */ jsxs("label", { children: [
341
- "Adoption anniversary (optional)",
342
- /* @__PURE__ */ jsx(
343
- "input",
344
- {
345
- type: "date",
346
- value: content.adoption_anniversary || "",
347
- onChange: (e) => change("adoption_anniversary", e.target.value || null)
348
- }
349
- )
350
- ] }),
351
- /* @__PURE__ */ jsxs("label", { children: [
352
- "Personality",
353
- /* @__PURE__ */ jsx(
354
- "input",
355
- {
356
- maxLength: 300,
357
- value: content.personality,
358
- onChange: (e) => change("personality", e.target.value)
359
- }
360
- )
271
+ }
272
+ setFieldErrors(errors);
273
+ if (first) {
274
+ first.focus();
275
+ return;
276
+ }
277
+ if (step === 0) {
278
+ setError("");
279
+ setStep(1);
280
+ requestAnimationFrame(() => {
281
+ formRef.current?.querySelector("h1")?.focus({ preventScroll: true });
282
+ formRef.current?.scrollIntoView({ block: "start" });
283
+ });
284
+ } else void save();
285
+ },
286
+ children: [
287
+ /* @__PURE__ */ jsxs("header", { className: "paw-form-intro", children: [
288
+ /* @__PURE__ */ jsxs("ol", { className: "paw-form-steps", "aria-label": "Submission progress", children: [
289
+ /* @__PURE__ */ jsx("li", { "aria-current": step === 0 ? "step" : void 0, children: "1. Their story" }),
290
+ /* @__PURE__ */ jsx("li", { "aria-current": step === 1 ? "step" : void 0, children: "2. Preview & permission" })
291
+ ] }),
292
+ /* @__PURE__ */ jsx("h1", { tabIndex: -1, children: kind === "celebration" && content.category === "memorial" ? "Remember a beloved companion." : ["lost", "found"].includes(kind) ? "Help them find home." : step === 1 ? "Looking good." : "Tell their story." }),
293
+ /* @__PURE__ */ jsx("p", { className: "paw-form-description", children: step === 0 ? content.category === "memorial" ? "Share a memory of a companion you\u2019ll always remember." : ["lost", "found"].includes(kind) ? "A clear description and an approximate location can help neighbors recognize a missing pet." : kind === "event" ? "Help neighbors discover your organization\u2019s pet community event." : "A favorite photograph. A little personality. A story your neighbors will love." : "Take a moment to check the details. You can always go back and make changes." }),
294
+ /* @__PURE__ */ jsx("p", { className: "paw-safety", children: "Choose a community, never a home address. Keep phone numbers, email addresses and microchip details out of your story." }),
295
+ /* @__PURE__ */ jsxs("div", { className: "paw-form-assurance", children: [
296
+ /* @__PURE__ */ jsx(PawMark, { size: 28 }),
297
+ /* @__PURE__ */ jsxs("p", { children: [
298
+ "Yours to share.",
299
+ /* @__PURE__ */ jsx("br", {}),
300
+ "Reviewed with care."
301
+ ] })
302
+ ] })
361
303
  ] }),
362
- /* @__PURE__ */ jsxs("label", { children: [
363
- "Favorite activities, separated by commas",
364
- /* @__PURE__ */ jsx(
365
- "input",
366
- {
367
- value: content.activities.join(", "),
368
- onChange: (e) => change(
369
- "activities",
370
- e.target.value.split(",").map((v) => v.trim()).slice(0, 8)
304
+ /* @__PURE__ */ jsxs("div", { className: "paw-form-body", children: [
305
+ step === 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
306
+ !initial && /* @__PURE__ */ jsxs(PetField, { children: [
307
+ "What would you like to share?",
308
+ /* @__PURE__ */ jsxs(
309
+ "select",
310
+ {
311
+ value: kind,
312
+ onChange: (e) => {
313
+ const k = e.target.value;
314
+ setKind(k);
315
+ setFieldErrors({});
316
+ setContent((c) => ({
317
+ ...c,
318
+ source: ["event", "adoption"].includes(k) ? {
319
+ organization: "",
320
+ official_url: "",
321
+ availability_confirmed: false
322
+ } : null,
323
+ event: k === "event" ? {
324
+ starts_at: "",
325
+ ends_at: "",
326
+ event_type: "adoption",
327
+ cost: "unknown",
328
+ cost_verified: false
329
+ } : null
330
+ }));
331
+ change(
332
+ "category",
333
+ k === "celebration" ? "birthday" : k === "photo" ? "cute" : ""
334
+ );
335
+ },
336
+ children: [
337
+ /* @__PURE__ */ jsx("option", { value: "spotlight", children: "Paw of the Week submission" }),
338
+ /* @__PURE__ */ jsx("option", { value: "profile", children: "Pet profile" }),
339
+ /* @__PURE__ */ jsx("option", { value: "photo", children: "Photo board" }),
340
+ /* @__PURE__ */ jsx("option", { value: "celebration", children: "Pet celebration" }),
341
+ /* @__PURE__ */ jsx("option", { value: "lost", children: "Lost pet report" }),
342
+ /* @__PURE__ */ jsx("option", { value: "found", children: "Found pet report" }),
343
+ /* @__PURE__ */ jsx("option", { value: "event", children: "Organization event" }),
344
+ /* @__PURE__ */ jsx("option", { value: "adoption", children: "Shelter or rescue adoption listing" })
345
+ ]
346
+ }
371
347
  )
372
- }
373
- )
374
- ] }),
375
- /* @__PURE__ */ jsxs("label", { children: [
376
- "Profile privacy",
377
- /* @__PURE__ */ jsxs(
378
- "select",
379
- {
380
- value: visibility,
381
- onChange: (e) => setVisibility(e.target.value),
382
- children: [
383
- /* @__PURE__ */ jsx("option", { value: "private", children: "Private \u2014 only me" }),
384
- /* @__PURE__ */ jsx("option", { value: "public", children: "Public \u2014 after editorial review" })
385
- ]
386
- }
387
- )
388
- ] })
389
- ] }),
390
- (kind === "photo" || kind === "celebration") && /* @__PURE__ */ jsxs("label", { children: [
391
- kind === "photo" ? "Photo category" : "Celebration",
392
- /* @__PURE__ */ jsx(
393
- "select",
394
- {
395
- value: content.category,
396
- onChange: (e) => change("category", e.target.value),
397
- children: (kind === "photo" ? PET_PHOTO_CATEGORIES : PET_CELEBRATIONS).map(([value, label]) => /* @__PURE__ */ jsx("option", { value, children: label }, value))
398
- }
399
- )
400
- ] }),
401
- ["lost", "found", "celebration"].includes(kind) && /* @__PURE__ */ jsxs("label", { children: [
402
- kind === "lost" ? "Date last seen" : kind === "found" ? "Date found" : "Celebration date (optional)",
403
- /* @__PURE__ */ jsx(
404
- "input",
405
- {
406
- required: kind !== "celebration",
407
- type: "date",
408
- value: content.date || "",
409
- onChange: (e) => change("date", e.target.value || null)
410
- }
411
- )
412
- ] }),
413
- /* @__PURE__ */ jsxs("label", { children: [
414
- kind === "profile" ? "A little biography" : "Your story",
415
- /* @__PURE__ */ jsx(
416
- "textarea",
417
- {
418
- required: kind !== "profile",
419
- maxLength: 4e3,
420
- value: content.story,
421
- onChange: (e) => change("story", e.target.value)
422
- }
423
- )
424
- ] }),
425
- kind === "adoption" && /* @__PURE__ */ jsxs("label", { children: [
426
- "Approximate age (optional)",
427
- /* @__PURE__ */ jsx(
428
- "input",
429
- {
430
- value: content.age_description || "",
431
- maxLength: 80,
432
- onChange: (e) => change("age_description", e.target.value)
433
- }
434
- )
435
- ] }),
436
- content.source && /* @__PURE__ */ jsxs("fieldset", { children: [
437
- /* @__PURE__ */ jsx("legend", { children: "Official organization and source" }),
438
- /* @__PURE__ */ jsxs("label", { children: [
439
- "Organization name",
440
- /* @__PURE__ */ jsx(
441
- "input",
442
- {
443
- required: true,
444
- maxLength: 200,
445
- value: content.source.organization,
446
- onChange: (e) => change("source", {
447
- ...content.source,
448
- organization: e.target.value
449
- })
450
- }
451
- )
452
- ] }),
453
- /* @__PURE__ */ jsxs("label", { children: [
454
- "Official listing or registration link",
455
- /* @__PURE__ */ jsx(
456
- "input",
457
- {
458
- required: true,
459
- type: "url",
460
- pattern: "https://.*",
461
- value: content.source.official_url,
462
- onChange: (e) => change("source", {
463
- ...content.source,
464
- official_url: e.target.value
465
- })
466
- }
467
- )
468
- ] }),
469
- /* @__PURE__ */ jsx("p", { children: "Editors verify the organization, permission to use photographs, and listing information. Availability and event costs are never assumed." })
470
- ] }),
471
- content.event && /* @__PURE__ */ jsxs("fieldset", { children: [
472
- /* @__PURE__ */ jsx("legend", { children: "Event schedule" }),
473
- /* @__PURE__ */ jsxs("p", { children: [
474
- "Enter times in your device\u2019s timezone (",
475
- deviceReady ? Intl.DateTimeFormat().resolvedOptions().timeZone : "your device timezone",
476
- "). They display in Pacific time."
477
- ] }),
478
- /* @__PURE__ */ jsxs("label", { children: [
479
- "Starts",
480
- /* @__PURE__ */ jsx(
481
- "input",
482
- {
483
- type: "datetime-local",
484
- required: true,
485
- value: deviceReady ? localDateTime(content.event.starts_at) : "",
486
- onChange: (e) => {
487
- if (e.target.value)
488
- change("event", {
489
- ...content.event,
490
- starts_at: new Date(e.target.value).toISOString()
491
- });
492
- }
493
- }
494
- )
495
- ] }),
496
- /* @__PURE__ */ jsxs("label", { children: [
497
- "Ends",
498
- /* @__PURE__ */ jsx(
499
- "input",
500
- {
501
- type: "datetime-local",
502
- required: true,
503
- value: deviceReady ? localDateTime(content.event.ends_at) : "",
504
- onChange: (e) => {
505
- if (e.target.value)
506
- change("event", {
507
- ...content.event,
508
- ends_at: new Date(e.target.value).toISOString()
509
- });
510
- }
511
- }
512
- )
513
- ] }),
514
- /* @__PURE__ */ jsxs("label", { children: [
515
- "Event type",
516
- /* @__PURE__ */ jsx(
517
- "select",
518
- {
519
- value: content.event.event_type,
520
- onChange: (e) => change("event", {
521
- ...content.event,
522
- event_type: e.target.value
523
- }),
524
- children: [
525
- "adoption",
526
- "veterinary",
527
- "vaccination",
528
- "walk",
529
- "festival",
530
- "fundraiser",
531
- "workshop",
532
- "celebration"
533
- ].map((type) => /* @__PURE__ */ jsx("option", { value: type, children: type }, type))
534
- }
535
- )
536
- ] }),
537
- /* @__PURE__ */ jsxs("label", { children: [
538
- "Reported cost",
539
- /* @__PURE__ */ jsxs(
540
- "select",
541
- {
542
- value: content.event.cost,
543
- onChange: (e) => change("event", {
544
- ...content.event,
545
- cost: e.target.value
546
- }),
547
- children: [
548
- /* @__PURE__ */ jsx("option", { value: "unknown", children: "Check with organization" }),
549
- /* @__PURE__ */ jsx("option", { value: "free", children: "Submitted as free \u2014 requires verification" }),
550
- /* @__PURE__ */ jsx("option", { value: "paid", children: "Paid registration" })
551
- ]
552
- }
553
- )
554
- ] })
555
- ] }),
556
- /* @__PURE__ */ jsxs("label", { children: [
557
- "Your public photo credit",
558
- /* @__PURE__ */ jsx(
559
- "input",
560
- {
561
- maxLength: 100,
562
- value: content.attribution,
563
- onChange: (e) => change("attribution", e.target.value),
564
- placeholder: "Name you want published"
565
- }
566
- )
567
- ] }),
568
- initial?.media.map((photo) => /* @__PURE__ */ jsxs("figure", { className: "paw-photo", children: [
569
- /* @__PURE__ */ jsx(
570
- "img",
571
- {
572
- src: `${apiPath}/submissions/${initial.id}/media/${photo.id}`,
573
- alt: photo.caption || content.name,
574
- width: 640,
575
- height: 480
576
- }
577
- ),
578
- /* @__PURE__ */ jsx("figcaption", { children: photo.attribution })
579
- ] }, photo.id)),
580
- /* @__PURE__ */ jsxs("label", { children: [
581
- "Authentic photographs",
582
- /* @__PURE__ */ jsx(
583
- "input",
584
- {
585
- type: "file",
586
- accept: "image/jpeg,image/png,image/webp",
587
- multiple: true,
588
- onChange: (e) => {
589
- const selected = Array.from(e.target.files || []);
590
- if (selected.length + (initial?.media.length || 0) > 10 || selected.some((f) => f.size > 8 * 1024 * 1024)) {
591
- setError(
592
- "Choose up to ten JPEG, PNG or WebP photographs, each under 8 MB."
593
- );
594
- return;
595
- }
596
- setError("");
597
- setFiles(
598
- selected.map((file) => ({
599
- file,
600
- key: crypto.randomUUID(),
601
- preview: URL.createObjectURL(file)
602
- }))
603
- );
604
- }
605
- }
606
- )
607
- ] }),
608
- /* @__PURE__ */ jsx("small", { children: "JPEG, PNG or WebP, up to 8 MB each. Photos are checked before publication and location metadata is removed." }),
609
- ["lost", "found"].includes(kind) && /* @__PURE__ */ jsxs("p", { className: "paw-safety", children: [
610
- PET_SAFETY.recovery,
611
- " Private recovery contact is not available yet."
612
- ] })
613
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
614
- /* @__PURE__ */ jsxs(
615
- "article",
616
- {
617
- className: content.category === "memorial" ? "paw-memorial" : "",
618
- children: [
619
- /* @__PURE__ */ jsx("p", { className: "paw-context", children: communities.find((c) => c.id === community)?.name }),
620
- /* @__PURE__ */ jsx("h2", { children: content.name }),
621
- !files.length && initial?.media[0] && /* @__PURE__ */ jsxs("figure", { className: "paw-photo", children: [
348
+ ] }),
349
+ !initial && kind !== "profile" && profiles.length > 0 && /* @__PURE__ */ jsxs(PetField, { children: [
350
+ "Start with one of your pets",
351
+ /* @__PURE__ */ jsxs(
352
+ "select",
353
+ {
354
+ value: profileId || "",
355
+ onChange: (e) => {
356
+ const p = profiles.find((p2) => p2.id === e.target.value);
357
+ setProfileId(p?.id || null);
358
+ if (p) {
359
+ setContent({
360
+ ...p.content,
361
+ category: content.category,
362
+ source: content.source,
363
+ event: content.event
364
+ });
365
+ setCommunity(
366
+ communities.some((c) => c.id === p.area_id) ? p.area_id : defaultCommunity || ""
367
+ );
368
+ }
369
+ },
370
+ children: [
371
+ /* @__PURE__ */ jsx("option", { value: "", children: "Enter pet details" }),
372
+ profiles.map((p) => /* @__PURE__ */ jsx("option", { value: p.id, children: p.content.name }, p.id))
373
+ ]
374
+ }
375
+ )
376
+ ] }),
377
+ /* @__PURE__ */ jsxs(PetField, { children: [
378
+ kind === "event" ? "Event title" : "Pet name",
622
379
  /* @__PURE__ */ jsx(
623
- "img",
380
+ "input",
624
381
  {
625
- src: `${apiPath}/submissions/${initial.id}/media/${initial.media[0].id}`,
626
- alt: initial.media[0].caption || content.name,
627
- width: 800,
628
- height: 600
382
+ required: true,
383
+ maxLength: 100,
384
+ value: content.name,
385
+ onChange: (e) => change("name", e.target.value),
386
+ autoComplete: "off"
629
387
  }
630
- ),
631
- /* @__PURE__ */ jsx("figcaption", { children: initial.media[0].attribution })
388
+ )
632
389
  ] }),
633
- files[0] && /* @__PURE__ */ jsxs("figure", { className: "paw-photo", children: [
390
+ /* @__PURE__ */ jsxs(PetField, { children: [
391
+ "Species",
392
+ /* @__PURE__ */ jsx(
393
+ "input",
394
+ {
395
+ required: true,
396
+ maxLength: 50,
397
+ value: content.species,
398
+ onChange: (e) => change("species", e.target.value)
399
+ }
400
+ )
401
+ ] }),
402
+ /* @__PURE__ */ jsxs(PetField, { children: [
403
+ "Home community",
404
+ /* @__PURE__ */ jsxs(
405
+ "select",
406
+ {
407
+ required: true,
408
+ value: community,
409
+ onChange: (e) => setCommunity(e.target.value),
410
+ children: [
411
+ /* @__PURE__ */ jsx("option", { value: "", children: "Choose a community" }),
412
+ communities.map((c) => /* @__PURE__ */ jsx("option", { value: c.id, children: c.name }, c.id))
413
+ ]
414
+ }
415
+ )
416
+ ] }),
417
+ kind === "profile" && /* @__PURE__ */ jsxs("fieldset", { children: [
418
+ /* @__PURE__ */ jsx("legend", { children: "A few favorite things" }),
419
+ /* @__PURE__ */ jsxs(PetField, { children: [
420
+ "Breed or mix (optional)",
421
+ /* @__PURE__ */ jsx(
422
+ "input",
423
+ {
424
+ value: content.breed,
425
+ maxLength: 100,
426
+ onChange: (e) => change("breed", e.target.value)
427
+ }
428
+ )
429
+ ] }),
430
+ /* @__PURE__ */ jsxs(PetField, { children: [
431
+ "Birthday (optional)",
432
+ /* @__PURE__ */ jsx(
433
+ "input",
434
+ {
435
+ type: "date",
436
+ value: content.birthday || "",
437
+ onChange: (e) => change("birthday", e.target.value || null)
438
+ }
439
+ )
440
+ ] }),
441
+ /* @__PURE__ */ jsxs(PetField, { children: [
442
+ "Adoption anniversary (optional)",
443
+ /* @__PURE__ */ jsx(
444
+ "input",
445
+ {
446
+ type: "date",
447
+ value: content.adoption_anniversary || "",
448
+ onChange: (e) => change("adoption_anniversary", e.target.value || null)
449
+ }
450
+ )
451
+ ] }),
452
+ /* @__PURE__ */ jsxs(PetField, { children: [
453
+ "Personality",
454
+ /* @__PURE__ */ jsx(
455
+ "input",
456
+ {
457
+ maxLength: 300,
458
+ value: content.personality,
459
+ onChange: (e) => change("personality", e.target.value)
460
+ }
461
+ )
462
+ ] }),
463
+ /* @__PURE__ */ jsxs(PetField, { children: [
464
+ "Favorite activities, separated by commas",
465
+ /* @__PURE__ */ jsx(
466
+ "input",
467
+ {
468
+ value: content.activities.join(", "),
469
+ onChange: (e) => change(
470
+ "activities",
471
+ e.target.value.split(",").map((v) => v.trim()).slice(0, 8)
472
+ )
473
+ }
474
+ )
475
+ ] }),
476
+ /* @__PURE__ */ jsxs(PetField, { children: [
477
+ "Profile privacy",
478
+ /* @__PURE__ */ jsxs(
479
+ "select",
480
+ {
481
+ value: visibility,
482
+ onChange: (e) => setVisibility(e.target.value),
483
+ children: [
484
+ /* @__PURE__ */ jsx("option", { value: "private", children: "Private \u2014 only me" }),
485
+ /* @__PURE__ */ jsx("option", { value: "public", children: "Public \u2014 after editorial review" })
486
+ ]
487
+ }
488
+ )
489
+ ] })
490
+ ] }),
491
+ (kind === "photo" || kind === "celebration") && /* @__PURE__ */ jsxs(PetField, { children: [
492
+ kind === "photo" ? "Photo category" : "Celebration",
493
+ /* @__PURE__ */ jsx(
494
+ "select",
495
+ {
496
+ value: content.category,
497
+ onChange: (e) => change("category", e.target.value),
498
+ children: (kind === "photo" ? PET_PHOTO_CATEGORIES : PET_CELEBRATIONS).map(([value, label]) => /* @__PURE__ */ jsx("option", { value, children: label }, value))
499
+ }
500
+ )
501
+ ] }),
502
+ ["lost", "found", "celebration"].includes(kind) && /* @__PURE__ */ jsxs(PetField, { children: [
503
+ kind === "lost" ? "Date last seen" : kind === "found" ? "Date found" : "Celebration date (optional)",
504
+ /* @__PURE__ */ jsx(
505
+ "input",
506
+ {
507
+ required: kind !== "celebration",
508
+ type: "date",
509
+ value: content.date || "",
510
+ onChange: (e) => change("date", e.target.value || null)
511
+ }
512
+ )
513
+ ] }),
514
+ /* @__PURE__ */ jsxs(PetField, { children: [
515
+ kind === "profile" ? "A little biography" : "Your story",
516
+ /* @__PURE__ */ jsx(
517
+ "textarea",
518
+ {
519
+ required: kind !== "profile",
520
+ maxLength: 4e3,
521
+ value: content.story,
522
+ onChange: (e) => change("story", e.target.value)
523
+ }
524
+ )
525
+ ] }),
526
+ kind === "adoption" && /* @__PURE__ */ jsxs(PetField, { children: [
527
+ "Approximate age (optional)",
528
+ /* @__PURE__ */ jsx(
529
+ "input",
530
+ {
531
+ value: content.age_description || "",
532
+ maxLength: 80,
533
+ onChange: (e) => change("age_description", e.target.value)
534
+ }
535
+ )
536
+ ] }),
537
+ content.source && /* @__PURE__ */ jsxs("fieldset", { children: [
538
+ /* @__PURE__ */ jsx("legend", { children: "Official organization and source" }),
539
+ /* @__PURE__ */ jsxs(PetField, { children: [
540
+ "Organization name",
541
+ /* @__PURE__ */ jsx(
542
+ "input",
543
+ {
544
+ required: true,
545
+ maxLength: 200,
546
+ value: content.source.organization,
547
+ onChange: (e) => change("source", {
548
+ ...content.source,
549
+ organization: e.target.value
550
+ })
551
+ }
552
+ )
553
+ ] }),
554
+ /* @__PURE__ */ jsxs(PetField, { children: [
555
+ "Official listing or registration link",
556
+ /* @__PURE__ */ jsx(
557
+ "input",
558
+ {
559
+ required: true,
560
+ type: "url",
561
+ pattern: "https://.*",
562
+ value: content.source.official_url,
563
+ onChange: (e) => change("source", {
564
+ ...content.source,
565
+ official_url: e.target.value
566
+ })
567
+ }
568
+ )
569
+ ] }),
570
+ /* @__PURE__ */ jsx("p", { children: "Editors verify the organization, permission to use photographs, and listing information. Availability and event costs are never assumed." })
571
+ ] }),
572
+ content.event && /* @__PURE__ */ jsxs("fieldset", { children: [
573
+ /* @__PURE__ */ jsx("legend", { children: "Event schedule" }),
574
+ /* @__PURE__ */ jsxs("p", { children: [
575
+ "Enter times in your device\u2019s timezone (",
576
+ deviceReady ? Intl.DateTimeFormat().resolvedOptions().timeZone : "your device timezone",
577
+ "). They display in Pacific time."
578
+ ] }),
579
+ /* @__PURE__ */ jsxs(PetField, { children: [
580
+ "Starts",
581
+ /* @__PURE__ */ jsx(
582
+ "input",
583
+ {
584
+ type: "datetime-local",
585
+ required: true,
586
+ value: deviceReady ? localDateTime(content.event.starts_at) : "",
587
+ onChange: (e) => {
588
+ if (e.target.value)
589
+ change("event", {
590
+ ...content.event,
591
+ starts_at: new Date(e.target.value).toISOString()
592
+ });
593
+ }
594
+ }
595
+ )
596
+ ] }),
597
+ /* @__PURE__ */ jsxs(PetField, { children: [
598
+ "Ends",
599
+ /* @__PURE__ */ jsx(
600
+ "input",
601
+ {
602
+ type: "datetime-local",
603
+ required: true,
604
+ value: deviceReady ? localDateTime(content.event.ends_at) : "",
605
+ onChange: (e) => {
606
+ if (e.target.value)
607
+ change("event", {
608
+ ...content.event,
609
+ ends_at: new Date(e.target.value).toISOString()
610
+ });
611
+ }
612
+ }
613
+ )
614
+ ] }),
615
+ /* @__PURE__ */ jsxs(PetField, { children: [
616
+ "Event type",
617
+ /* @__PURE__ */ jsx(
618
+ "select",
619
+ {
620
+ value: content.event.event_type,
621
+ onChange: (e) => change("event", {
622
+ ...content.event,
623
+ event_type: e.target.value
624
+ }),
625
+ children: [
626
+ "adoption",
627
+ "veterinary",
628
+ "vaccination",
629
+ "walk",
630
+ "festival",
631
+ "fundraiser",
632
+ "workshop",
633
+ "celebration"
634
+ ].map((type) => /* @__PURE__ */ jsx("option", { value: type, children: type }, type))
635
+ }
636
+ )
637
+ ] }),
638
+ /* @__PURE__ */ jsxs(PetField, { children: [
639
+ "Reported cost",
640
+ /* @__PURE__ */ jsxs(
641
+ "select",
642
+ {
643
+ value: content.event.cost,
644
+ onChange: (e) => change("event", {
645
+ ...content.event,
646
+ cost: e.target.value
647
+ }),
648
+ children: [
649
+ /* @__PURE__ */ jsx("option", { value: "unknown", children: "Check with organization" }),
650
+ /* @__PURE__ */ jsx("option", { value: "free", children: "Submitted as free \u2014 requires verification" }),
651
+ /* @__PURE__ */ jsx("option", { value: "paid", children: "Paid registration" })
652
+ ]
653
+ }
654
+ )
655
+ ] })
656
+ ] }),
657
+ /* @__PURE__ */ jsxs(PetField, { children: [
658
+ "Your public photo credit",
659
+ /* @__PURE__ */ jsx(
660
+ "input",
661
+ {
662
+ maxLength: 100,
663
+ value: content.attribution,
664
+ onChange: (e) => change("attribution", e.target.value),
665
+ placeholder: "Name you want published"
666
+ }
667
+ )
668
+ ] }),
669
+ initial?.media.map((photo) => /* @__PURE__ */ jsxs("figure", { className: "paw-photo", children: [
634
670
  /* @__PURE__ */ jsx(
635
671
  "img",
636
672
  {
637
- src: files[0].preview,
638
- alt: `Private preview of ${content.name}`,
639
- width: 800,
640
- height: 600
673
+ src: `${apiPath}/submissions/${initial.id}/media/${photo.id}`,
674
+ alt: photo.caption || content.name,
675
+ width: 640,
676
+ height: 480
677
+ }
678
+ ),
679
+ /* @__PURE__ */ jsx("figcaption", { children: photo.attribution })
680
+ ] }, photo.id)),
681
+ /* @__PURE__ */ jsxs(PetField, { className: "paw-photo-picker", children: [
682
+ "Authentic photographs",
683
+ /* @__PURE__ */ jsx(
684
+ "input",
685
+ {
686
+ type: "file",
687
+ className: "paw-upload-input",
688
+ required: ["spotlight", "photo", "adoption"].includes(kind) && !files.length && !initial?.media.length,
689
+ "aria-describedby": "paw-photo-guidance",
690
+ accept: "image/jpeg,image/png,image/webp",
691
+ multiple: true,
692
+ onChange: (e) => {
693
+ const selected = Array.from(e.target.files || []);
694
+ if (selected.length + (initial?.media.length || 0) > 10 || selected.some(
695
+ (f) => f.size > 8 * 1024 * 1024 || !["image/jpeg", "image/png", "image/webp"].includes(
696
+ f.type
697
+ )
698
+ )) {
699
+ setError(
700
+ "Choose up to ten JPEG, PNG or WebP photographs, each under 8 MB."
701
+ );
702
+ return;
703
+ }
704
+ setError("");
705
+ setFiles(
706
+ selected.map((file) => ({
707
+ file,
708
+ key: crypto.randomUUID(),
709
+ preview: URL.createObjectURL(file)
710
+ }))
711
+ );
712
+ }
641
713
  }
642
714
  ),
643
- /* @__PURE__ */ jsx("figcaption", { children: content.attribution })
715
+ /* @__PURE__ */ jsxs("span", { className: "paw-upload-prompt", children: [
716
+ /* @__PURE__ */ jsxs(
717
+ "svg",
718
+ {
719
+ viewBox: "0 0 32 32",
720
+ width: "32",
721
+ height: "32",
722
+ fill: "none",
723
+ stroke: "currentColor",
724
+ strokeWidth: "1.5",
725
+ "aria-hidden": "true",
726
+ children: [
727
+ /* @__PURE__ */ jsx("path", { d: "M11 8l2-3h6l2 3h5a3 3 0 013 3v13a3 3 0 01-3 3H6a3 3 0 01-3-3V11a3 3 0 013-3z" }),
728
+ /* @__PURE__ */ jsx("circle", { cx: "16", cy: "17", r: "6" })
729
+ ]
730
+ }
731
+ ),
732
+ /* @__PURE__ */ jsx("strong", { children: files.length ? `${files.length} ${files.length === 1 ? "photo" : "photos"} selected` : content.category === "memorial" ? "A photograph to remember them by." : ["lost", "found"].includes(kind) ? "A clear photograph helps neighbors recognize them." : "Their best angle. Their biggest personality." }),
733
+ /* @__PURE__ */ jsx("span", { children: files.length ? "Choose different photos" : "Choose photos from your device" })
734
+ ] })
644
735
  ] }),
645
- /* @__PURE__ */ jsx("p", { style: { whiteSpace: "pre-wrap" }, children: content.story })
646
- ]
647
- }
648
- ),
649
- /* @__PURE__ */ jsx("p", { children: kind === "profile" && visibility === "private" ? "This profile will remain private." : "This is a private preview. Editors will review your contribution before anyone else can see it." }),
650
- needsPermission && /* @__PURE__ */ jsxs(Fragment, { children: [
651
- /* @__PURE__ */ jsxs("details", { children: [
652
- /* @__PURE__ */ jsx("summary", { children: "Publication permission and guidelines" }),
653
- /* @__PURE__ */ jsx("p", { children: policy.text }),
654
- /* @__PURE__ */ jsx("p", { children: policy.guidelines })
655
- ] }),
656
- /* @__PURE__ */ jsxs("label", { className: "paw-consent", children: [
657
- /* @__PURE__ */ jsx(
658
- "input",
659
- {
660
- type: "checkbox",
661
- required: true,
662
- checked: consent,
663
- onChange: (e) => setConsent(e.target.checked)
664
- }
665
- ),
666
- /* @__PURE__ */ jsx("span", { children: "I have read and accept the publication permission. These are authentic photographs I have permission to share." })
736
+ /* @__PURE__ */ jsx("small", { id: "paw-photo-guidance", children: "JPEG, PNG or WebP, up to 8 MB each. Photos are checked before publication and location metadata is removed." }),
737
+ files.length > 0 && /* @__PURE__ */ jsx(
738
+ "div",
739
+ {
740
+ className: "paw-upload-previews",
741
+ "aria-label": "Selected photographs",
742
+ children: files.map((file) => /* @__PURE__ */ jsxs("figure", { children: [
743
+ /* @__PURE__ */ jsx(
744
+ "img",
745
+ {
746
+ src: file.preview,
747
+ alt: `Selected photograph: ${file.file.name}`,
748
+ width: 160,
749
+ height: 160
750
+ }
751
+ ),
752
+ /* @__PURE__ */ jsx("figcaption", { children: file.file.name })
753
+ ] }, file.key))
754
+ }
755
+ ),
756
+ ["lost", "found"].includes(kind) && /* @__PURE__ */ jsxs("p", { className: "paw-safety", children: [
757
+ PET_SAFETY.recovery,
758
+ " Private recovery contact is not available yet."
759
+ ] })
760
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
761
+ /* @__PURE__ */ jsxs(
762
+ "article",
763
+ {
764
+ className: content.category === "memorial" ? "paw-memorial" : "",
765
+ children: [
766
+ /* @__PURE__ */ jsx("p", { className: "paw-context", children: communities.find((c) => c.id === community)?.name }),
767
+ /* @__PURE__ */ jsx("h2", { children: content.name }),
768
+ !files.length && initial?.media[0] && /* @__PURE__ */ jsxs("figure", { className: "paw-photo", children: [
769
+ /* @__PURE__ */ jsx(
770
+ "img",
771
+ {
772
+ src: `${apiPath}/submissions/${initial.id}/media/${initial.media[0].id}`,
773
+ alt: initial.media[0].caption || content.name,
774
+ width: 800,
775
+ height: 600
776
+ }
777
+ ),
778
+ /* @__PURE__ */ jsx("figcaption", { children: initial.media[0].attribution })
779
+ ] }),
780
+ files[0] && /* @__PURE__ */ jsxs("figure", { className: "paw-photo", children: [
781
+ /* @__PURE__ */ jsx(
782
+ "img",
783
+ {
784
+ src: files[0].preview,
785
+ alt: `Private preview of ${content.name}`,
786
+ width: 800,
787
+ height: 600
788
+ }
789
+ ),
790
+ /* @__PURE__ */ jsx("figcaption", { children: content.attribution })
791
+ ] }),
792
+ /* @__PURE__ */ jsx("p", { style: { whiteSpace: "pre-wrap" }, children: content.story })
793
+ ]
794
+ }
795
+ ),
796
+ /* @__PURE__ */ jsx("p", { children: kind === "profile" && visibility === "private" ? "This profile will remain private." : "This is a private preview. Editors will review your contribution before anyone else can see it." }),
797
+ needsPermission && /* @__PURE__ */ jsxs(Fragment, { children: [
798
+ /* @__PURE__ */ jsxs("details", { children: [
799
+ /* @__PURE__ */ jsx("summary", { children: "Publication permission and guidelines" }),
800
+ /* @__PURE__ */ jsx("p", { children: policy.text }),
801
+ /* @__PURE__ */ jsx("p", { children: policy.guidelines })
802
+ ] }),
803
+ /* @__PURE__ */ jsxs(PetField, { className: "paw-consent", children: [
804
+ /* @__PURE__ */ jsx(
805
+ "input",
806
+ {
807
+ type: "checkbox",
808
+ required: true,
809
+ checked: consent,
810
+ onChange: (e) => setConsent(e.target.checked)
811
+ }
812
+ ),
813
+ /* @__PURE__ */ jsx("span", { children: "I have read and accept the publication permission. These are authentic photographs I have permission to share." })
814
+ ] })
815
+ ] })
816
+ ] }),
817
+ Object.keys(fieldErrors).length > 0 && /* @__PURE__ */ jsx("p", { role: "alert", className: "paw-validation-summary", children: "A few details need your attention. Check the fields above." }),
818
+ error && /* @__PURE__ */ jsx("p", { role: "alert", className: "paw-error", children: error }),
819
+ busy && /* @__PURE__ */ jsxs(PetField, { children: [
820
+ "Saving your contribution",
821
+ /* @__PURE__ */ jsx("progress", { value: progress, max: files.length + 2 })
822
+ ] }),
823
+ /* @__PURE__ */ jsxs("div", { className: "paw-form-actions", children: [
824
+ step === 1 && /* @__PURE__ */ jsx(
825
+ "button",
826
+ {
827
+ type: "button",
828
+ disabled: busy,
829
+ onClick: () => {
830
+ setFieldErrors({});
831
+ setStep(0);
832
+ formRef.current?.scrollIntoView({ block: "start" });
833
+ },
834
+ children: "Keep editing"
835
+ }
836
+ ),
837
+ /* @__PURE__ */ jsx("button", { disabled: busy, type: "submit", children: busy ? "Saving\u2026" : step === 0 ? "Preview your story" : kind === "profile" && visibility === "private" ? "Save private profile" : "Submit for editorial review" })
838
+ ] }),
839
+ /* @__PURE__ */ jsx("p", { className: "paw-form-endnote", children: step === 0 ? "Next: a private preview. Nothing is published yet." : kind === "profile" && visibility === "private" ? "Only you can see this private profile." : "Your contribution stays private until editorial approval." })
667
840
  ] })
668
- ] })
669
- ] }),
670
- error && /* @__PURE__ */ jsx("p", { role: "alert", className: "paw-error", children: error }),
671
- busy && /* @__PURE__ */ jsxs("label", { children: [
672
- "Saving your contribution",
673
- /* @__PURE__ */ jsx("progress", { value: progress, max: files.length + 2 })
674
- ] }),
675
- /* @__PURE__ */ jsxs("div", { className: "paw-form-actions", children: [
676
- step === 1 && /* @__PURE__ */ jsx("button", { type: "button", disabled: busy, onClick: () => setStep(0), children: "Keep editing" }),
677
- /* @__PURE__ */ jsx("button", { disabled: busy, type: "submit", children: busy ? "Saving\u2026" : step === 0 ? "Preview your story" : kind === "profile" && visibility === "private" ? "Save private profile" : "Submit for editorial review" })
678
- ] })
679
- ]
841
+ ]
842
+ }
843
+ )
680
844
  }
681
845
  );
682
846
  }