create-nextblock 0.15.10 → 0.16.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.
- package/package.json +1 -1
- package/templates/nextblock-template/app/api/ai/seo/alt-text/route.ts +221 -0
- package/templates/nextblock-template/app/api/ai/seo/metadata/route.ts +186 -0
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +193 -1
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +7 -1
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +27 -0
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +406 -229
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +171 -6
- package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +254 -245
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +177 -2
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +28 -0
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +649 -406
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +33 -0
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +618 -383
- package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -0
- package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -0
- package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -0
- package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -0
- package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -0
- package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -0
- package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -0
- package/templates/nextblock-template/app/layout.tsx +1 -1
- package/templates/nextblock-template/app/robots.ts +123 -0
- package/templates/nextblock-template/app/sitemap.ts +1 -1
- package/templates/nextblock-template/components/seo/GenerateMetaButton.tsx +137 -0
- package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +244 -0
- package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -0
- package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -0
- package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -0
- package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -0
- package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -0
- package/templates/nextblock-template/lib/cortex-ai/alt-text-request.ts +86 -0
- package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -0
- package/templates/nextblock-template/lib/seo/alt-text-write-back.test.ts +154 -0
- package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -0
- package/templates/nextblock-template/lib/seo/block-content.ts +123 -0
- package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -0
- package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -0
- package/templates/nextblock-template/lib/seo/page-audit-context.tsx +140 -0
- package/templates/nextblock-template/lib/seo/page-document.test.ts +350 -0
- package/templates/nextblock-template/lib/seo/page-document.ts +412 -0
- package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -0
- package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -0
- package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -0
- package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -0
- package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -0
- package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -0
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +10 -0
- package/templates/nextblock-template/next-env.d.ts +2 -2
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/proxy.ts +240 -20
- package/templates/nextblock-template/app/robots.txt/route.ts +0 -32
|
@@ -1,245 +1,254 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { type KeyboardEvent, useCallback, useEffect, useState } from "react";
|
|
4
|
-
import Image from "next/image";
|
|
5
|
-
import {
|
|
6
|
-
Button,
|
|
7
|
-
Dialog,
|
|
8
|
-
DialogClose,
|
|
9
|
-
DialogContent,
|
|
10
|
-
DialogFooter,
|
|
11
|
-
DialogHeader,
|
|
12
|
-
DialogTitle,
|
|
13
|
-
DialogTrigger,
|
|
14
|
-
Input,
|
|
15
|
-
Label,
|
|
16
|
-
Separator,
|
|
17
|
-
Spinner,
|
|
18
|
-
} from "@nextblock-cms/ui";
|
|
19
|
-
import type { Database } from "@nextblock-cms/db";
|
|
20
|
-
|
|
21
|
-
import { resolveMediaUrl } from "../../../lib/media/resolveMediaUrl";
|
|
22
|
-
import { getMediaItems } from "../media/actions";
|
|
23
|
-
import MediaImage from "../media/components/MediaImage";
|
|
24
|
-
import MediaUploadForm from "../media/components/MediaUploadForm";
|
|
25
|
-
|
|
26
|
-
type Media = Database["public"]["Tables"]["media"]["Row"];
|
|
27
|
-
|
|
28
|
-
interface FeatureImageFieldProps {
|
|
29
|
-
initialImageId?: string | null;
|
|
30
|
-
initialImageUrl?: string | null;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const [
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
{
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type KeyboardEvent, useCallback, useEffect, useState } from "react";
|
|
4
|
+
import Image from "next/image";
|
|
5
|
+
import {
|
|
6
|
+
Button,
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogClose,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogFooter,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
DialogTrigger,
|
|
14
|
+
Input,
|
|
15
|
+
Label,
|
|
16
|
+
Separator,
|
|
17
|
+
Spinner,
|
|
18
|
+
} from "@nextblock-cms/ui";
|
|
19
|
+
import type { Database } from "@nextblock-cms/db";
|
|
20
|
+
|
|
21
|
+
import { resolveMediaUrl } from "../../../lib/media/resolveMediaUrl";
|
|
22
|
+
import { getMediaItems } from "../media/actions";
|
|
23
|
+
import MediaImage from "../media/components/MediaImage";
|
|
24
|
+
import MediaUploadForm from "../media/components/MediaUploadForm";
|
|
25
|
+
|
|
26
|
+
type Media = Database["public"]["Tables"]["media"]["Row"];
|
|
27
|
+
|
|
28
|
+
interface FeatureImageFieldProps {
|
|
29
|
+
initialImageId?: string | null;
|
|
30
|
+
initialImageUrl?: string | null;
|
|
31
|
+
/**
|
|
32
|
+
* Reports a selection or a removal to the parent form.
|
|
33
|
+
*
|
|
34
|
+
* The resolved URL is handed over alongside the id because this field has already
|
|
35
|
+
* computed it for its own thumbnail, and the parent otherwise has no way to learn it
|
|
36
|
+
* until the server round-trips the new `feature_image_id` back into
|
|
37
|
+
* `initialImageUrl`. `PageForm` and `PostForm` use it to keep their share-card preview
|
|
38
|
+
* honest the instant an image is picked; the id remains the only persisted half.
|
|
39
|
+
*/
|
|
40
|
+
onImageIdChange?: (imageId: string | null, imageUrl: string | null) => void;
|
|
41
|
+
uploadFolder: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default function FeatureImageField({
|
|
45
|
+
initialImageId,
|
|
46
|
+
initialImageUrl,
|
|
47
|
+
onImageIdChange,
|
|
48
|
+
uploadFolder,
|
|
49
|
+
}: FeatureImageFieldProps) {
|
|
50
|
+
const [selectedFeatureImage, setSelectedFeatureImage] = useState<{
|
|
51
|
+
id: string | null;
|
|
52
|
+
url: string | null;
|
|
53
|
+
}>({
|
|
54
|
+
id: initialImageId || null,
|
|
55
|
+
url: initialImageUrl || null,
|
|
56
|
+
});
|
|
57
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
58
|
+
const [mediaItems, setMediaItems] = useState<Media[]>([]);
|
|
59
|
+
const [mediaLoading, setMediaLoading] = useState(false);
|
|
60
|
+
const [mediaError, setMediaError] = useState<string | null>(null);
|
|
61
|
+
const [mediaPage, setMediaPage] = useState(1);
|
|
62
|
+
const [hasMoreMedia, setHasMoreMedia] = useState(true);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
setSelectedFeatureImage({
|
|
66
|
+
id: initialImageId || null,
|
|
67
|
+
url: initialImageUrl || null,
|
|
68
|
+
});
|
|
69
|
+
}, [initialImageId, initialImageUrl]);
|
|
70
|
+
|
|
71
|
+
const loadMedia = useCallback(
|
|
72
|
+
async (pageToLoad = 1, append = false) => {
|
|
73
|
+
setMediaLoading(true);
|
|
74
|
+
setMediaError(null);
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const result = await getMediaItems(pageToLoad, 20);
|
|
78
|
+
if (result.error) {
|
|
79
|
+
setMediaError(result.error);
|
|
80
|
+
if (!append) setMediaItems([]);
|
|
81
|
+
} else if (result.data) {
|
|
82
|
+
setMediaItems((prev) =>
|
|
83
|
+
append ? [...prev, ...(result.data || [])] : result.data || []
|
|
84
|
+
);
|
|
85
|
+
setHasMoreMedia(result.hasMore !== undefined ? result.hasMore : false);
|
|
86
|
+
setMediaPage(pageToLoad);
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
setMediaError("An unexpected error occurred while fetching media.");
|
|
90
|
+
if (!append) setMediaItems([]);
|
|
91
|
+
} finally {
|
|
92
|
+
setMediaLoading(false);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
[]
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (isModalOpen) {
|
|
100
|
+
setMediaPage(1);
|
|
101
|
+
setHasMoreMedia(true);
|
|
102
|
+
loadMedia(1, false);
|
|
103
|
+
}
|
|
104
|
+
}, [isModalOpen, loadMedia]);
|
|
105
|
+
|
|
106
|
+
const handleImageSelectInModal = (image: Media) => {
|
|
107
|
+
const imageUrl = resolveMediaUrl(image.file_path || image.object_key);
|
|
108
|
+
|
|
109
|
+
if (!imageUrl) {
|
|
110
|
+
setMediaError("Selected image is missing a valid path.");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
setSelectedFeatureImage({ id: image.id, url: imageUrl });
|
|
115
|
+
onImageIdChange?.(image.id, imageUrl);
|
|
116
|
+
setIsModalOpen(false);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const handleRemoveImage = () => {
|
|
120
|
+
setSelectedFeatureImage({ id: null, url: null });
|
|
121
|
+
onImageIdChange?.(null, null);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleImageSelectKeyDown = (event: KeyboardEvent<HTMLDivElement>, image: Media) => {
|
|
125
|
+
if (event.key !== "Enter" && event.key !== " ") {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
handleImageSelectInModal(image);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div>
|
|
135
|
+
<Label htmlFor="feature_image_id">Feature Image</Label>
|
|
136
|
+
<Input type="hidden" id="feature_image_id" name="feature_image_id" value={selectedFeatureImage.id || ""} />
|
|
137
|
+
<div className="mt-2">
|
|
138
|
+
{selectedFeatureImage.url && (
|
|
139
|
+
<div className="mb-4">
|
|
140
|
+
<Image
|
|
141
|
+
src={selectedFeatureImage.url}
|
|
142
|
+
alt="Selected feature image"
|
|
143
|
+
width={200}
|
|
144
|
+
height={200}
|
|
145
|
+
className="rounded-md object-cover"
|
|
146
|
+
/>
|
|
147
|
+
<Button
|
|
148
|
+
type="button"
|
|
149
|
+
variant="link"
|
|
150
|
+
className="mt-2 px-0 text-red-600"
|
|
151
|
+
onClick={handleRemoveImage}
|
|
152
|
+
>
|
|
153
|
+
Remove Image
|
|
154
|
+
</Button>
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
|
158
|
+
<DialogTrigger asChild>
|
|
159
|
+
<Button type="button" variant="outline">
|
|
160
|
+
{selectedFeatureImage.id ? "Change Feature Image" : "Select Feature Image"}
|
|
161
|
+
</Button>
|
|
162
|
+
</DialogTrigger>
|
|
163
|
+
<DialogContent className="flex max-h-[90vh] flex-col sm:max-w-[90vw]">
|
|
164
|
+
<DialogHeader>
|
|
165
|
+
<DialogTitle>Select Feature Image</DialogTitle>
|
|
166
|
+
</DialogHeader>
|
|
167
|
+
<div className="p-1">
|
|
168
|
+
<MediaUploadForm
|
|
169
|
+
compact
|
|
170
|
+
returnJustData={true}
|
|
171
|
+
defaultFolder={uploadFolder}
|
|
172
|
+
onUploadSuccess={(newlyUploadedMedia) => {
|
|
173
|
+
setMediaItems((prevItems) => [
|
|
174
|
+
newlyUploadedMedia,
|
|
175
|
+
...prevItems.filter((item) => item.id !== newlyUploadedMedia.id),
|
|
176
|
+
]);
|
|
177
|
+
handleImageSelectInModal(newlyUploadedMedia);
|
|
178
|
+
}}
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
<Separator className="my-4" />
|
|
182
|
+
<div className="flex-grow overflow-y-auto py-4" id="media-modal-scroll-area">
|
|
183
|
+
{mediaLoading && mediaItems.length === 0 && (
|
|
184
|
+
<p className="text-center text-muted-foreground">Loading media...</p>
|
|
185
|
+
)}
|
|
186
|
+
{mediaError && <p className="text-center text-red-600">{mediaError}</p>}
|
|
187
|
+
{!mediaLoading && !mediaError && mediaItems.length === 0 && (
|
|
188
|
+
<p className="text-center text-muted-foreground">No media items found. Try uploading some first.</p>
|
|
189
|
+
)}
|
|
190
|
+
|
|
191
|
+
{mediaItems.length > 0 && (
|
|
192
|
+
<div className="grid grid-cols-[repeat(auto-fill,minmax(0,150px))] gap-3">
|
|
193
|
+
{mediaItems.map((item) => {
|
|
194
|
+
const imageUrl = resolveMediaUrl(item.file_path || item.object_key);
|
|
195
|
+
|
|
196
|
+
if (!item.file_type?.startsWith("image/") || !imageUrl) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div
|
|
202
|
+
key={item.id}
|
|
203
|
+
className="group relative aspect-square cursor-pointer overflow-hidden rounded-lg border bg-muted/20 shadow-sm transition-all hover:ring-2 hover:ring-primary"
|
|
204
|
+
onClick={() => handleImageSelectInModal(item)}
|
|
205
|
+
onKeyDown={(event) => handleImageSelectKeyDown(event, item)}
|
|
206
|
+
tabIndex={0}
|
|
207
|
+
role="button"
|
|
208
|
+
aria-label={`Select ${item.file_name}`}
|
|
209
|
+
>
|
|
210
|
+
<MediaImage
|
|
211
|
+
src={imageUrl}
|
|
212
|
+
alt={item.description || item.file_name}
|
|
213
|
+
width={item.width || 300}
|
|
214
|
+
height={item.height || 300}
|
|
215
|
+
blurDataURL={item.blur_data_url}
|
|
216
|
+
className="h-full w-full object-cover transition-transform group-hover:scale-105"
|
|
217
|
+
/>
|
|
218
|
+
<div className="absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-black/60 via-black/10 to-transparent p-2 opacity-0 transition-opacity group-hover:opacity-100">
|
|
219
|
+
<p className="truncate text-xs text-white" title={item.file_name}>
|
|
220
|
+
{item.file_name}
|
|
221
|
+
</p>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
);
|
|
225
|
+
})}
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
{!mediaLoading && hasMoreMedia && mediaItems.length > 0 && (
|
|
229
|
+
<div className="mt-6 text-center">
|
|
230
|
+
<Button type="button" onClick={() => loadMedia(mediaPage + 1, true)} variant="outline" disabled={mediaLoading}>
|
|
231
|
+
{mediaLoading ? (
|
|
232
|
+
<>
|
|
233
|
+
<Spinner className="mr-2 h-4 w-4" /> Loading...
|
|
234
|
+
</>
|
|
235
|
+
) : (
|
|
236
|
+
"Load More"
|
|
237
|
+
)}
|
|
238
|
+
</Button>
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
</div>
|
|
242
|
+
<DialogFooter className="mt-auto border-t pt-4">
|
|
243
|
+
<DialogClose asChild>
|
|
244
|
+
<Button type="button" variant="outline" onClick={() => setMediaError(null)}>
|
|
245
|
+
Cancel
|
|
246
|
+
</Button>
|
|
247
|
+
</DialogClose>
|
|
248
|
+
</DialogFooter>
|
|
249
|
+
</DialogContent>
|
|
250
|
+
</Dialog>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|