@morphika/andami 0.5.11 → 0.8.1

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,173 +1,487 @@
1
- "use client";
2
-
3
- import { useState, useEffect } from "react";
4
- import { getSiteConfig } from "../../lib/config";
5
-
6
- interface MetadataData {
7
- default_title: string;
8
- default_description: string;
9
- default_og_image: string;
10
- favicon_path: string;
11
- analytics_id: string;
12
- }
13
-
14
- interface MetadataEditorProps {
15
- initialData: MetadataData;
16
- onSave: (data: MetadataData) => Promise<void>;
17
- saving: boolean;
18
- }
19
-
20
- export default function MetadataEditor({
21
- initialData,
22
- onSave,
23
- saving,
24
- }: MetadataEditorProps) {
25
- const [title, setTitle] = useState(initialData.default_title);
26
- const [description, setDescription] = useState(
27
- initialData.default_description
28
- );
29
- const [ogImage, setOgImage] = useState(initialData.default_og_image);
30
- const [favicon, setFavicon] = useState(initialData.favicon_path);
31
- const [analyticsId, setAnalyticsId] = useState(initialData.analytics_id);
32
-
33
- useEffect(() => {
34
- setTitle(initialData.default_title);
35
- setDescription(initialData.default_description);
36
- setOgImage(initialData.default_og_image);
37
- setFavicon(initialData.favicon_path);
38
- setAnalyticsId(initialData.analytics_id);
39
- }, [initialData]);
40
-
41
- const handleSave = () => {
42
- onSave({
43
- default_title: title,
44
- default_description: description,
45
- default_og_image: ogImage,
46
- favicon_path: favicon,
47
- analytics_id: analyticsId,
48
- });
49
- };
50
-
51
- return (
52
- <section className="space-y-4">
53
- <div className="flex items-center justify-between border-b border-neutral-200 pb-2">
54
- <h2 className="text-base font-semibold text-neutral-800">
55
- Site Metadata
56
- </h2>
57
- </div>
58
-
59
- <p className="text-xs text-neutral-400">
60
- Default SEO metadata used when pages don&apos;t specify their own.
61
- Affects search results and social sharing.
62
- </p>
63
-
64
- {/* Title */}
65
- <div className="space-y-1">
66
- <label className="text-xs font-medium text-neutral-500">
67
- Default Page Title
68
- </label>
69
- <input
70
- type="text"
71
- value={title}
72
- onChange={(e) => setTitle(e.target.value)}
73
- placeholder={getSiteConfig().defaults.metaTitle}
74
- className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#3580f9] focus:outline-none"
75
- />
76
- <p className="text-xs text-neutral-400">
77
- Shown in browser tabs and search results when no page-specific title is
78
- set.
79
- </p>
80
- </div>
81
-
82
- {/* Description */}
83
- <div className="space-y-1">
84
- <label className="text-xs font-medium text-neutral-500">
85
- Default Description
86
- </label>
87
- <textarea
88
- value={description}
89
- onChange={(e) => setDescription(e.target.value)}
90
- placeholder="Motion graphics studio based in Barcelona..."
91
- rows={3}
92
- className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#3580f9] focus:outline-none resize-none"
93
- />
94
- <div className="flex justify-between">
95
- <p className="text-xs text-neutral-400">
96
- Used in search results and social media previews.
97
- </p>
98
- <span
99
- className={`text-xs ${
100
- description.length > 160 ? "text-red-400" : "text-neutral-400"
101
- }`}
102
- >
103
- {description.length}/160
104
- </span>
105
- </div>
106
- </div>
107
-
108
- {/* OG Image */}
109
- <div className="space-y-1">
110
- <label className="text-xs font-medium text-neutral-500">
111
- Default OG Image Path
112
- </label>
113
- <input
114
- type="text"
115
- value={ogImage}
116
- onChange={(e) => setOgImage(e.target.value)}
117
- placeholder="meta/og-image.jpg"
118
- className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#3580f9] focus:outline-none"
119
- />
120
- <p className="text-xs text-neutral-400">
121
- Relative path to the default image shown when sharing on social media.
122
- Resolved via the asset seed URL.
123
- </p>
124
- </div>
125
-
126
- {/* Favicon */}
127
- <div className="space-y-1">
128
- <label className="text-xs font-medium text-neutral-500">
129
- Favicon Path
130
- </label>
131
- <input
132
- type="text"
133
- value={favicon}
134
- onChange={(e) => setFavicon(e.target.value)}
135
- placeholder="meta/favicon.ico"
136
- className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#3580f9] focus:outline-none"
137
- />
138
- <p className="text-xs text-neutral-400">
139
- Relative path to the favicon. Resolved via the asset seed URL.
140
- </p>
141
- </div>
142
-
143
- {/* Analytics ID */}
144
- <div className="space-y-1">
145
- <label className="text-xs font-medium text-neutral-500">
146
- Analytics ID (optional)
147
- </label>
148
- <input
149
- type="text"
150
- value={analyticsId}
151
- onChange={(e) => setAnalyticsId(e.target.value)}
152
- placeholder="G-XXXXXXXXXX or plausible domain"
153
- className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#3580f9] focus:outline-none"
154
- />
155
- <p className="text-xs text-neutral-400">
156
- Google Analytics measurement ID or Plausible domain. Leave empty to
157
- disable analytics.
158
- </p>
159
- </div>
160
-
161
- {/* Save */}
162
- <div className="flex justify-end">
163
- <button
164
- onClick={handleSave}
165
- disabled={saving}
166
- className="rounded-lg bg-[#3580f9] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#2d6dd4] transition-colors disabled:opacity-50"
167
- >
168
- {saving ? "Saving..." : "Save Metadata"}
169
- </button>
170
- </div>
171
- </section>
172
- );
173
- }
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { getSiteConfig } from "../../lib/config";
5
+ import { AssetPathInput } from "../builder/editors/shared";
6
+ import SERPPreview from "./SERPPreview";
7
+
8
+ const TITLE_LIMIT = 60;
9
+ const DESCRIPTION_LIMIT = 160;
10
+
11
+ export interface SocialLinkInput {
12
+ _key?: string;
13
+ label?: string;
14
+ url: string;
15
+ }
16
+
17
+ export interface MetadataData {
18
+ default_title: string;
19
+ default_description: string;
20
+ default_og_image: string;
21
+ favicon_path: string;
22
+ analytics_id: string;
23
+ org_logo: string;
24
+ default_author: string;
25
+ social_links: SocialLinkInput[];
26
+ founding_year: number | undefined;
27
+ address_locality: string;
28
+ address_country: string;
29
+ contact_email: string;
30
+ keywords: string[];
31
+ verification_google: string;
32
+ verification_bing: string;
33
+ }
34
+
35
+ interface MetadataEditorProps {
36
+ initialData: MetadataData;
37
+ onSave: (data: MetadataData) => Promise<void>;
38
+ saving: boolean;
39
+ }
40
+
41
+ const INPUT_CLASS =
42
+ "w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#3580f9] focus:outline-none";
43
+
44
+ const SECTION_TITLE_CLASS =
45
+ "text-sm font-semibold text-neutral-800 pt-6 border-t border-neutral-200";
46
+
47
+ export default function MetadataEditor({
48
+ initialData,
49
+ onSave,
50
+ saving,
51
+ }: MetadataEditorProps) {
52
+ const [title, setTitle] = useState(initialData.default_title);
53
+ const [description, setDescription] = useState(initialData.default_description);
54
+ const [ogImage, setOgImage] = useState(initialData.default_og_image);
55
+ const [favicon, setFavicon] = useState(initialData.favicon_path);
56
+ const [analyticsId, setAnalyticsId] = useState(initialData.analytics_id);
57
+ const [orgLogo, setOrgLogo] = useState(initialData.org_logo);
58
+ const [defaultAuthor, setDefaultAuthor] = useState(initialData.default_author);
59
+ const [socialLinks, setSocialLinks] = useState<SocialLinkInput[]>(
60
+ initialData.social_links || []
61
+ );
62
+ const [foundingYear, setFoundingYear] = useState<number | undefined>(
63
+ initialData.founding_year
64
+ );
65
+ const [addressLocality, setAddressLocality] = useState(initialData.address_locality);
66
+ const [addressCountry, setAddressCountry] = useState(initialData.address_country);
67
+ const [contactEmail, setContactEmail] = useState(initialData.contact_email);
68
+ const [keywordsText, setKeywordsText] = useState(
69
+ (initialData.keywords || []).join(", ")
70
+ );
71
+ const [verGoogle, setVerGoogle] = useState(initialData.verification_google);
72
+ const [verBing, setVerBing] = useState(initialData.verification_bing);
73
+
74
+ useEffect(() => {
75
+ setTitle(initialData.default_title);
76
+ setDescription(initialData.default_description);
77
+ setOgImage(initialData.default_og_image);
78
+ setFavicon(initialData.favicon_path);
79
+ setAnalyticsId(initialData.analytics_id);
80
+ setOrgLogo(initialData.org_logo);
81
+ setDefaultAuthor(initialData.default_author);
82
+ setSocialLinks(initialData.social_links || []);
83
+ setFoundingYear(initialData.founding_year);
84
+ setAddressLocality(initialData.address_locality);
85
+ setAddressCountry(initialData.address_country);
86
+ setContactEmail(initialData.contact_email);
87
+ setKeywordsText((initialData.keywords || []).join(", "));
88
+ setVerGoogle(initialData.verification_google);
89
+ setVerBing(initialData.verification_bing);
90
+ }, [initialData]);
91
+
92
+ const handleSave = () => {
93
+ // Parse keywords from comma-separated input, trim + dedupe
94
+ const keywords = Array.from(
95
+ new Set(
96
+ keywordsText
97
+ .split(",")
98
+ .map((k) => k.trim())
99
+ .filter(Boolean)
100
+ )
101
+ );
102
+ onSave({
103
+ default_title: title,
104
+ default_description: description,
105
+ default_og_image: ogImage,
106
+ favicon_path: favicon,
107
+ analytics_id: analyticsId,
108
+ org_logo: orgLogo,
109
+ default_author: defaultAuthor,
110
+ social_links: socialLinks.filter((l) => l.url?.trim()),
111
+ founding_year: foundingYear,
112
+ address_locality: addressLocality,
113
+ address_country: addressCountry,
114
+ contact_email: contactEmail,
115
+ keywords,
116
+ verification_google: verGoogle,
117
+ verification_bing: verBing,
118
+ });
119
+ };
120
+
121
+ const updateSocialLink = (i: number, patch: Partial<SocialLinkInput>) => {
122
+ setSocialLinks((arr) =>
123
+ arr.map((l, idx) => (idx === i ? { ...l, ...patch } : l))
124
+ );
125
+ };
126
+ const removeSocialLink = (i: number) =>
127
+ setSocialLinks((arr) => arr.filter((_, idx) => idx !== i));
128
+ const addSocialLink = () =>
129
+ setSocialLinks((arr) => [...arr, { url: "", label: "" }]);
130
+
131
+ const titleOver = title.length > TITLE_LIMIT;
132
+ const descOver = description.length > DESCRIPTION_LIMIT;
133
+
134
+ return (
135
+ <section className="space-y-4">
136
+ <div className="flex items-center justify-between border-b border-neutral-200 pb-2">
137
+ <h2 className="text-base font-semibold text-neutral-800">
138
+ Site Metadata
139
+ </h2>
140
+ </div>
141
+
142
+ <p className="text-xs text-neutral-400">
143
+ Default SEO metadata used when pages don&apos;t specify their own.
144
+ Affects search results and social sharing.
145
+ </p>
146
+
147
+ {/* ============================================================
148
+ DEFAULTS
149
+ ============================================================ */}
150
+
151
+ <div className="space-y-1">
152
+ <label className="text-xs font-medium text-neutral-500">
153
+ Default Page Title
154
+ </label>
155
+ <input
156
+ type="text"
157
+ value={title}
158
+ onChange={(e) => setTitle(e.target.value)}
159
+ placeholder={getSiteConfig().defaults.metaTitle}
160
+ className={INPUT_CLASS}
161
+ />
162
+ <div className="flex justify-between">
163
+ <p className="text-xs text-neutral-400">
164
+ Shown in browser tabs and search results when no page-specific title is set.
165
+ </p>
166
+ <span className={`text-xs ${titleOver ? "text-red-400" : "text-neutral-400"}`}>
167
+ {title.length}/{TITLE_LIMIT}
168
+ </span>
169
+ </div>
170
+ </div>
171
+
172
+ <div className="space-y-1">
173
+ <label className="text-xs font-medium text-neutral-500">
174
+ Default Description
175
+ </label>
176
+ <textarea
177
+ value={description}
178
+ onChange={(e) => setDescription(e.target.value)}
179
+ placeholder="Motion graphics studio based in Barcelona..."
180
+ rows={3}
181
+ className={`${INPUT_CLASS} resize-none`}
182
+ />
183
+ <div className="flex justify-between">
184
+ <p className="text-xs text-neutral-400">
185
+ Used in search results and social media previews.
186
+ </p>
187
+ <span className={`text-xs ${descOver ? "text-red-400" : "text-neutral-400"}`}>
188
+ {description.length}/{DESCRIPTION_LIMIT}
189
+ </span>
190
+ </div>
191
+ </div>
192
+
193
+ <SERPPreview title={title} description={description} />
194
+
195
+ <div className="space-y-1">
196
+ <label className="text-xs font-medium text-neutral-500">
197
+ Default OG Image
198
+ </label>
199
+ <AssetPathInput
200
+ value={ogImage}
201
+ onChange={setOgImage}
202
+ placeholder="meta/og-image.jpg"
203
+ filterType="image"
204
+ />
205
+ <p className="text-xs text-neutral-400">
206
+ Default image used when sharing on social media (Facebook, Twitter, LinkedIn). Recommended size: 1200×630px. Pages can override per-page.
207
+ </p>
208
+ </div>
209
+
210
+ <div className="space-y-1">
211
+ <label className="text-xs font-medium text-neutral-500">Favicon</label>
212
+ <AssetPathInput
213
+ value={favicon}
214
+ onChange={setFavicon}
215
+ placeholder="meta/favicon.ico"
216
+ filterType="image"
217
+ />
218
+ <p className="text-xs text-neutral-400">
219
+ Icon shown in browser tabs and bookmarks. Square images recommended (16×16 .ico or 32×32+ .png/.svg).
220
+ </p>
221
+ </div>
222
+
223
+ <div className="space-y-1">
224
+ <label className="text-xs font-medium text-neutral-500">
225
+ Analytics ID (optional)
226
+ </label>
227
+ <input
228
+ type="text"
229
+ value={analyticsId}
230
+ onChange={(e) => setAnalyticsId(e.target.value)}
231
+ placeholder="G-XXXXXXXXXX or plausible domain"
232
+ className={INPUT_CLASS}
233
+ />
234
+ <p className="text-xs text-neutral-400">
235
+ Google Analytics measurement ID or Plausible domain. Leave empty to disable analytics.
236
+ </p>
237
+ </div>
238
+
239
+ {/* ============================================================
240
+ SOCIAL & BRANDING
241
+ ============================================================ */}
242
+
243
+ <h3 className={SECTION_TITLE_CLASS}>Social & Branding</h3>
244
+ <p className="text-xs text-neutral-400">
245
+ Used by structured data (schema.org) so Google can show your brand in
246
+ Knowledge Panels and link your social profiles.
247
+ </p>
248
+
249
+ <div className="space-y-1">
250
+ <label className="text-xs font-medium text-neutral-500">
251
+ Organization Logo
252
+ </label>
253
+ <AssetPathInput
254
+ value={orgLogo}
255
+ onChange={setOrgLogo}
256
+ placeholder="meta/logo.png"
257
+ filterType="image"
258
+ />
259
+ <p className="text-xs text-neutral-400">
260
+ Logo for Google&apos;s Knowledge Panel and rich results. Square (min 112×112, ideal 500×500).
261
+ </p>
262
+ </div>
263
+
264
+ <div className="space-y-1">
265
+ <label className="text-xs font-medium text-neutral-500">
266
+ Default Author
267
+ </label>
268
+ <input
269
+ type="text"
270
+ value={defaultAuthor}
271
+ onChange={(e) => setDefaultAuthor(e.target.value)}
272
+ placeholder={getSiteConfig().name}
273
+ className={INPUT_CLASS}
274
+ />
275
+ <p className="text-xs text-neutral-400">
276
+ Author name used on project pages when not overridden. Typically your studio name.
277
+ </p>
278
+ </div>
279
+
280
+ <div className="space-y-2">
281
+ <label className="text-xs font-medium text-neutral-500">
282
+ Social Profiles
283
+ </label>
284
+ <p className="text-xs text-neutral-400">
285
+ Profile URLs (Instagram, LinkedIn, Behance, Vimeo, etc.). Emitted as schema.org sameAs[] — helps Google link your organization to its social presence.
286
+ </p>
287
+ <div className="space-y-2">
288
+ {socialLinks.map((link, i) => (
289
+ <div key={link._key || i} className="flex gap-1.5 items-start">
290
+ <input
291
+ type="text"
292
+ value={link.label || ""}
293
+ onChange={(e) => updateSocialLink(i, { label: e.target.value })}
294
+ placeholder="Label (e.g. Instagram)"
295
+ className={`${INPUT_CLASS} flex-[1]`}
296
+ />
297
+ <input
298
+ type="url"
299
+ value={link.url || ""}
300
+ onChange={(e) => updateSocialLink(i, { url: e.target.value })}
301
+ placeholder="https://instagram.com/yourhandle"
302
+ className={`${INPUT_CLASS} flex-[2]`}
303
+ />
304
+ <button
305
+ type="button"
306
+ onClick={() => removeSocialLink(i)}
307
+ className="shrink-0 rounded-lg px-2.5 py-2.5 text-xs text-neutral-400 hover:text-red-500 hover:bg-red-50 transition-colors"
308
+ aria-label="Remove social link"
309
+ >
310
+
311
+ </button>
312
+ </div>
313
+ ))}
314
+ </div>
315
+ <button
316
+ type="button"
317
+ onClick={addSocialLink}
318
+ className="text-xs text-[#3580f9] hover:underline"
319
+ >
320
+ + Add Profile
321
+ </button>
322
+ </div>
323
+
324
+ {/* ============================================================
325
+ ORGANIZATION INFO (citation-friendly facts for LLMs + JSON-LD)
326
+ ============================================================ */}
327
+
328
+ <h3 className={SECTION_TITLE_CLASS}>Organization Info</h3>
329
+ <p className="text-xs text-neutral-400">
330
+ Citation-friendly facts emitted as schema.org structured data and in
331
+ the <code>/llms.txt</code> summary. AI assistants quote these directly
332
+ when answering questions about your organization.
333
+ </p>
334
+
335
+ <div className="grid grid-cols-2 gap-4">
336
+ <div className="space-y-1">
337
+ <label className="text-xs font-medium text-neutral-500">
338
+ Founded (year)
339
+ </label>
340
+ <input
341
+ type="number"
342
+ value={foundingYear ?? ""}
343
+ onChange={(e) => {
344
+ const v = e.target.value;
345
+ setFoundingYear(v === "" ? undefined : Number(v));
346
+ }}
347
+ min={1800}
348
+ max={new Date().getFullYear()}
349
+ placeholder="2018"
350
+ className={INPUT_CLASS}
351
+ />
352
+ </div>
353
+ <div className="space-y-1">
354
+ <label className="text-xs font-medium text-neutral-500">
355
+ Contact Email
356
+ </label>
357
+ <input
358
+ type="email"
359
+ value={contactEmail}
360
+ onChange={(e) => setContactEmail(e.target.value)}
361
+ placeholder="hello@example.com"
362
+ className={INPUT_CLASS}
363
+ />
364
+ </div>
365
+ </div>
366
+
367
+ <div className="grid grid-cols-2 gap-4">
368
+ <div className="space-y-1">
369
+ <label className="text-xs font-medium text-neutral-500">
370
+ City
371
+ </label>
372
+ <input
373
+ type="text"
374
+ value={addressLocality}
375
+ onChange={(e) => setAddressLocality(e.target.value)}
376
+ placeholder="Barcelona"
377
+ className={INPUT_CLASS}
378
+ />
379
+ </div>
380
+ <div className="space-y-1">
381
+ <label className="text-xs font-medium text-neutral-500">
382
+ Country
383
+ </label>
384
+ <input
385
+ type="text"
386
+ value={addressCountry}
387
+ onChange={(e) => setAddressCountry(e.target.value)}
388
+ placeholder="Spain"
389
+ className={INPUT_CLASS}
390
+ />
391
+ </div>
392
+ </div>
393
+
394
+ <div className="space-y-1">
395
+ <label className="text-xs font-medium text-neutral-500">
396
+ Areas of Expertise
397
+ </label>
398
+ <input
399
+ type="text"
400
+ value={keywordsText}
401
+ onChange={(e) => setKeywordsText(e.target.value)}
402
+ placeholder="CGI, Motion Graphics, 3D Animation"
403
+ className={INPUT_CLASS}
404
+ />
405
+ <p className="text-xs text-neutral-400">
406
+ Comma-separated list of services / specialties. Emitted as
407
+ schema.org knowsAbout — helps LLMs cite you for related queries.
408
+ </p>
409
+ </div>
410
+
411
+ {/* ============================================================
412
+ SEARCH ENGINE VERIFICATION
413
+ ============================================================ */}
414
+
415
+ <h3 className={SECTION_TITLE_CLASS}>Search Engine Verification</h3>
416
+ <p className="text-xs text-neutral-400">
417
+ Connect your site to free webmaster tools to see how it performs in
418
+ search. Paste the verification code from each tool below (just the
419
+ code, not the full <code>&lt;meta&gt;</code> tag).
420
+ </p>
421
+
422
+ <div className="space-y-1">
423
+ <label className="text-xs font-medium text-neutral-500">
424
+ Google Search Console
425
+ </label>
426
+ <input
427
+ type="text"
428
+ value={verGoogle}
429
+ onChange={(e) => setVerGoogle(e.target.value)}
430
+ placeholder="abc123-verification-code-from-google"
431
+ className={INPUT_CLASS}
432
+ />
433
+ <p className="text-xs text-neutral-400">
434
+ From{" "}
435
+ <a
436
+ href="https://search.google.com/search-console"
437
+ target="_blank"
438
+ rel="noopener noreferrer"
439
+ className="text-[#3580f9] hover:underline"
440
+ >
441
+ Search Console
442
+ </a>{" "}
443
+ → Settings → Ownership verification → HTML tag method.
444
+ </p>
445
+ </div>
446
+
447
+ <div className="space-y-1">
448
+ <label className="text-xs font-medium text-neutral-500">
449
+ Bing Webmaster
450
+ </label>
451
+ <input
452
+ type="text"
453
+ value={verBing}
454
+ onChange={(e) => setVerBing(e.target.value)}
455
+ placeholder="abc123-verification-code-from-bing"
456
+ className={INPUT_CLASS}
457
+ />
458
+ <p className="text-xs text-neutral-400">
459
+ From{" "}
460
+ <a
461
+ href="https://www.bing.com/webmasters"
462
+ target="_blank"
463
+ rel="noopener noreferrer"
464
+ className="text-[#3580f9] hover:underline"
465
+ >
466
+ Bing Webmaster Tools
467
+ </a>{" "}
468
+ → Site verification → Meta tag method.
469
+ </p>
470
+ </div>
471
+
472
+ {/* ============================================================
473
+ SAVE
474
+ ============================================================ */}
475
+
476
+ <div className="flex justify-end pt-4">
477
+ <button
478
+ onClick={handleSave}
479
+ disabled={saving}
480
+ className="rounded-lg bg-[#3580f9] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#2d6dd4] transition-colors disabled:opacity-50"
481
+ >
482
+ {saving ? "Saving..." : "Save Metadata"}
483
+ </button>
484
+ </div>
485
+ </section>
486
+ );
487
+ }