strapi-plugin-copy-any-component 1.0.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,995 @@
1
+ import React, { useState, useEffect, useCallback } from "react";
2
+ import {
3
+ Main,
4
+ Box,
5
+ SingleSelect,
6
+ SingleSelectOption,
7
+ Typography,
8
+ Alert,
9
+ Flex,
10
+ Badge,
11
+ Button,
12
+ Card,
13
+ Grid,
14
+ IconButton,
15
+ } from "@strapi/design-system";
16
+ import { Drag, Cross } from "@strapi/icons";
17
+ import { Page } from "@strapi/strapi/admin";
18
+ import { useFetchClient } from "@strapi/strapi/admin";
19
+ import { PLUGIN_ID } from "../pluginId";
20
+
21
+ const HomePage = () => {
22
+ const [pages, setPages] = useState([]);
23
+ const [sourcePageId, setSourcePageId] = useState("");
24
+ const [targetPageId, setTargetPageId] = useState("");
25
+ const [sourceSections, setSourceSections] = useState([]);
26
+ const [targetSections, setTargetSections] = useState([]);
27
+ const [loading, setLoading] = useState(false);
28
+ const [loadingSourceSections, setLoadingSourceSections] = useState(false);
29
+ const [loadingTargetSections, setLoadingTargetSections] = useState(false);
30
+ const [message, setMessage] = useState(null);
31
+ const [draggedIndex, setDraggedIndex] = useState(null);
32
+ const [draggedSource, setDraggedSource] = useState(null);
33
+ const [isDragOver, setIsDragOver] = useState(false);
34
+ const [dragOverTargetIndex, setDragOverTargetIndex] = useState(null);
35
+ const [copyDetails, setCopyDetails] = useState(null);
36
+ const [showDetailsModal, setShowDetailsModal] = useState(false);
37
+ const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
38
+ const [publishing, setPublishing] = useState(false);
39
+
40
+ // Content Type yapılandırması
41
+ const [contentTypes, setContentTypes] = useState([]);
42
+ const [currentConfig, setCurrentConfig] = useState({ contentType: '', dynamicZoneField: '' });
43
+ const [selectedContentType, setSelectedContentType] = useState('');
44
+ const [selectedDynamicZone, setSelectedDynamicZone] = useState('');
45
+ const [showSettings, setShowSettings] = useState(false);
46
+ const [configLoading, setConfigLoading] = useState(false);
47
+
48
+ const { get, post, put } = useFetchClient();
49
+
50
+ // Content type'ları yükle
51
+ useEffect(() => {
52
+ const fetchContentTypes = async () => {
53
+ try {
54
+ const { data } = await get(`/${PLUGIN_ID}/content-types`);
55
+ if (data?.data) {
56
+ setContentTypes(data.data.contentTypes || []);
57
+ setCurrentConfig(data.data.currentConfig || {});
58
+ setSelectedContentType(data.data.currentConfig?.contentType || '');
59
+ setSelectedDynamicZone(data.data.currentConfig?.dynamicZoneField || '');
60
+ }
61
+ } catch (error) {
62
+ console.error("Failed to load content types:", error);
63
+ }
64
+ };
65
+ fetchContentTypes();
66
+ }, [get]);
67
+
68
+ // Sayfaları yükle
69
+ useEffect(() => {
70
+ const fetchPages = async () => {
71
+ try {
72
+ const { data } = await get(`/${PLUGIN_ID}/pages`);
73
+ setPages(data?.data || []);
74
+ } catch (error) {
75
+ setMessage({ type: "danger", text: "Failed to load pages: " + (error.message || "Unknown error") });
76
+ }
77
+ };
78
+ fetchPages();
79
+ }, [get, currentConfig]);
80
+
81
+ // Content type yapılandırmasını güncelle
82
+ const handleConfigUpdate = async () => {
83
+ if (!selectedContentType || !selectedDynamicZone) {
84
+ setMessage({ type: "warning", text: "Lütfen content type ve dynamic zone seçin" });
85
+ return;
86
+ }
87
+
88
+ setConfigLoading(true);
89
+ try {
90
+ const { data } = await put(`/${PLUGIN_ID}/config`, {
91
+ contentType: selectedContentType,
92
+ dynamicZoneField: selectedDynamicZone,
93
+ });
94
+
95
+ if (data?.data) {
96
+ setCurrentConfig({
97
+ contentType: selectedContentType,
98
+ dynamicZoneField: selectedDynamicZone,
99
+ });
100
+ setMessage({ type: "success", text: "Yapılandırma güncellendi! Sayfalar yeniden yükleniyor..." });
101
+
102
+ // Sayfaları yeniden yükle
103
+ const pagesRes = await get(`/${PLUGIN_ID}/pages`);
104
+ setPages(pagesRes.data?.data || []);
105
+ setSourcePageId("");
106
+ setTargetPageId("");
107
+ setSourceSections([]);
108
+ setTargetSections([]);
109
+ setShowSettings(false);
110
+ }
111
+ } catch (error) {
112
+ setMessage({ type: "danger", text: "Yapılandırma güncellenemedi: " + (error.message || "Bilinmeyen hata") });
113
+ } finally {
114
+ setConfigLoading(false);
115
+ }
116
+ };
117
+
118
+ // Seçilen content type'a göre dynamic zone'ları getir
119
+ const getAvailableDynamicZones = () => {
120
+ const ct = contentTypes.find(c => c.uid === selectedContentType);
121
+ return ct?.dynamicZones || [];
122
+ };
123
+
124
+ useEffect(() => {
125
+ if (sourcePageId) {
126
+ loadSourceSections();
127
+ } else {
128
+ setSourceSections([]);
129
+ }
130
+ }, [sourcePageId]);
131
+
132
+ useEffect(() => {
133
+ if (targetPageId) {
134
+ loadTargetSections();
135
+ setHasUnsavedChanges(false);
136
+ } else {
137
+ setTargetSections([]);
138
+ setHasUnsavedChanges(false);
139
+ }
140
+ }, [targetPageId]);
141
+
142
+ const loadSourceSections = useCallback(async () => {
143
+ setLoadingSourceSections(true);
144
+ try {
145
+ const { data } = await get(
146
+ `/${PLUGIN_ID}/pages/${encodeURIComponent(sourcePageId)}/sections`
147
+ );
148
+ if (data.error) {
149
+ setMessage({ type: "danger", text: data.error });
150
+ setSourceSections([]);
151
+ } else {
152
+ setSourceSections(data.data.sections || []);
153
+ }
154
+ } catch (error) {
155
+ setMessage({ type: "danger", text: "Failed to load sections: " + (error.message || "Unknown error") });
156
+ setSourceSections([]);
157
+ } finally {
158
+ setLoadingSourceSections(false);
159
+ }
160
+ }, [sourcePageId, get]);
161
+
162
+ const loadTargetSections = useCallback(async () => {
163
+ setLoadingTargetSections(true);
164
+ try {
165
+ const { data } = await get(
166
+ `/${PLUGIN_ID}/pages/${encodeURIComponent(targetPageId)}/sections`
167
+ );
168
+ if (data.error) {
169
+ setTargetSections([]);
170
+ } else {
171
+ setTargetSections(data.data.sections || []);
172
+ }
173
+ } catch (error) {
174
+ setTargetSections([]);
175
+ } finally {
176
+ setLoadingTargetSections(false);
177
+ }
178
+ }, [targetPageId, get]);
179
+
180
+ const handleDragStart = useCallback((e, index, source = 'source') => {
181
+ setDraggedIndex(index);
182
+ setDraggedSource(source);
183
+ e.dataTransfer.effectAllowed = source === 'source' ? "copy" : "move";
184
+ e.dataTransfer.setData("text/plain", JSON.stringify({ index, source }));
185
+
186
+ const dragImage = e.target.cloneNode(true);
187
+ dragImage.style.opacity = "0.8";
188
+ dragImage.style.position = "absolute";
189
+ dragImage.style.top = "-1000px";
190
+ document.body.appendChild(dragImage);
191
+ e.dataTransfer.setDragImage(dragImage, 0, 0);
192
+ setTimeout(() => document.body.removeChild(dragImage), 0);
193
+ }, []);
194
+
195
+ const handleDragEnd = useCallback(() => {
196
+ setDraggedIndex(null);
197
+ setDraggedSource(null);
198
+ setIsDragOver(false);
199
+ setDragOverTargetIndex(null);
200
+ }, []);
201
+
202
+ const handleDragOver = useCallback((e, targetIndex = null) => {
203
+ e.preventDefault();
204
+ e.stopPropagation();
205
+ e.dataTransfer.dropEffect = draggedSource === 'source' ? "copy" : "move";
206
+ setIsDragOver(true);
207
+ if (targetIndex !== null) {
208
+ setDragOverTargetIndex(targetIndex);
209
+ }
210
+ }, [draggedSource]);
211
+
212
+ const handleDragLeave = useCallback((e) => {
213
+ e.preventDefault();
214
+ e.stopPropagation();
215
+ const rect = e.currentTarget.getBoundingClientRect();
216
+ const x = e.clientX;
217
+ const y = e.clientY;
218
+ if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) {
219
+ setIsDragOver(false);
220
+ setDragOverTargetIndex(null);
221
+ }
222
+ }, []);
223
+
224
+ const handleDrop = useCallback(async (e, dropIndex = null) => {
225
+ e.preventDefault();
226
+ e.stopPropagation();
227
+ setIsDragOver(false);
228
+ setDragOverTargetIndex(null);
229
+
230
+ const dataStr = e.dataTransfer.getData("text/plain");
231
+ let dragData;
232
+ try {
233
+ dragData = JSON.parse(dataStr);
234
+ } catch {
235
+ const index = parseInt(dataStr, 10);
236
+ if (isNaN(index)) return;
237
+ dragData = { index, source: 'source' };
238
+ }
239
+
240
+ const { index, source } = dragData;
241
+
242
+ if (source === 'target' && dropIndex !== null && targetPageId) {
243
+ if (index === dropIndex) {
244
+ return;
245
+ }
246
+
247
+ const newSections = [...targetSections];
248
+ const [movedSection] = newSections.splice(index, 1);
249
+ newSections.splice(dropIndex, 0, movedSection);
250
+
251
+ setTargetSections(newSections);
252
+ setHasUnsavedChanges(true);
253
+ setDraggedIndex(null);
254
+ setDraggedSource(null);
255
+ return;
256
+ }
257
+
258
+ if (!sourcePageId || !targetPageId) {
259
+ setMessage({ type: "danger", text: "Please select both source and target pages" });
260
+ return;
261
+ }
262
+
263
+ setLoading(true);
264
+ setMessage(null);
265
+
266
+ try {
267
+ const { data } = await post(
268
+ `/${PLUGIN_ID}/pages/${encodeURIComponent(sourcePageId)}/copy-to/${encodeURIComponent(targetPageId)}`,
269
+ { sectionIndices: [index], insertIndex: dropIndex }
270
+ );
271
+
272
+ if (data.error) {
273
+ setMessage({ type: "danger", text: data.error });
274
+ setCopyDetails(null);
275
+ } else {
276
+ const isDuplicate = sourcePageId === targetPageId;
277
+ setMessage({
278
+ type: "success",
279
+ text: `Successfully ${isDuplicate ? 'duplicated' : 'copied'} ${data.data.copiedSectionsCount} section(s)! Click to view details.`,
280
+ });
281
+ setCopyDetails(data.data.copiedDetails || []);
282
+ setHasUnsavedChanges(true);
283
+ setShowDetailsModal(true);
284
+
285
+ await loadTargetSections();
286
+ if (isDuplicate) {
287
+ await loadSourceSections();
288
+ }
289
+ }
290
+ } catch (error) {
291
+ setMessage({
292
+ type: "danger",
293
+ text: error.message || "An error occurred",
294
+ });
295
+ } finally {
296
+ setLoading(false);
297
+ setDraggedIndex(null);
298
+ setDraggedSource(null);
299
+ }
300
+ }, [sourcePageId, targetPageId, targetSections, post, put, loadTargetSections, loadSourceSections]);
301
+
302
+ const getPageTitle = (page) => {
303
+ // Farklı content type'larda farklı field adları olabilir
304
+ return page.attributes?.title || page.title ||
305
+ page.attributes?.name || page.name ||
306
+ page.attributes?.heading || page.heading ||
307
+ page.attributes?.label || page.label ||
308
+ page.attributes?.slug || page.slug ||
309
+ `ID: ${page.id}`;
310
+ };
311
+
312
+ const handlePublish = useCallback(async () => {
313
+ if (!targetPageId || !hasUnsavedChanges) {
314
+ return;
315
+ }
316
+
317
+ setPublishing(true);
318
+ setMessage(null);
319
+
320
+ try {
321
+ const { data: updateData } = await put(
322
+ `/${PLUGIN_ID}/pages/${encodeURIComponent(targetPageId)}/sections`,
323
+ { sections: targetSections }
324
+ );
325
+
326
+ if (updateData.error) {
327
+ setMessage({ type: "danger", text: updateData.error });
328
+ setPublishing(false);
329
+ return;
330
+ }
331
+
332
+ const { data: publishData } = await post(
333
+ `/${PLUGIN_ID}/pages/${encodeURIComponent(targetPageId)}/publish`
334
+ );
335
+
336
+ if (publishData.error) {
337
+ setMessage({ type: "danger", text: publishData.error });
338
+ } else {
339
+ setMessage({
340
+ type: "success",
341
+ text: "Page published successfully!",
342
+ });
343
+ setHasUnsavedChanges(false);
344
+ await loadTargetSections();
345
+ }
346
+ } catch (error) {
347
+ setMessage({
348
+ type: "danger",
349
+ text: error.message || "An error occurred while publishing",
350
+ });
351
+ } finally {
352
+ setPublishing(false);
353
+ }
354
+ }, [targetPageId, targetSections, hasUnsavedChanges, put, post, loadTargetSections]);
355
+
356
+ const getComponentName = (section) => {
357
+ const componentType = section.__component || "";
358
+ return componentType.replace("sections.", "").replace(/-/g, " ").replace(/\b\w/g, l => l.toUpperCase());
359
+ };
360
+
361
+ const getComponentPreview = (section) => {
362
+ return section.title ||
363
+ section.heading ||
364
+ section.text?.substring(0, 40) ||
365
+ section.content?.substring(0, 40) ||
366
+ "No content";
367
+ };
368
+
369
+ return (
370
+ <Page.Main>
371
+ <Page.Title>Copy Any Component 🎨</Page.Title>
372
+ <Main>
373
+ <Box padding={8}>
374
+ <Flex justifyContent="space-between" alignItems="flex-start" marginBottom={6}>
375
+ <Box>
376
+ <Typography variant="alpha" tag="h1">
377
+ Copy Any Component 🎨
378
+ </Typography>
379
+ <Typography variant="omega" textColor="neutral600" marginTop={2}>
380
+ Drag any component from source page to target page - Works with all component types!
381
+ </Typography>
382
+ {currentConfig.contentType && (
383
+ <Flex gap={2} marginTop={2}>
384
+ <Badge>
385
+ {contentTypes.find(c => c.uid === currentConfig.contentType)?.displayName || currentConfig.contentType}
386
+ </Badge>
387
+ <Badge variant="secondary">
388
+ {currentConfig.dynamicZoneField}
389
+ </Badge>
390
+ </Flex>
391
+ )}
392
+ </Box>
393
+ <Button
394
+ variant={showSettings ? "secondary" : "tertiary"}
395
+ onClick={() => setShowSettings(!showSettings)}
396
+ size="S"
397
+ >
398
+ ⚙️ {showSettings ? "Ayarları Kapat" : "Content Type Ayarları"}
399
+ </Button>
400
+ </Flex>
401
+
402
+ {/* Content Type Ayarları */}
403
+ {showSettings && (
404
+ <Box marginBottom={6} padding={4} background="neutral100" hasRadius>
405
+ <Typography variant="beta" tag="h3" marginBottom={4}>
406
+ 📋 Content Type Yapılandırması
407
+ </Typography>
408
+ <Typography variant="omega" textColor="neutral600" marginBottom={4}>
409
+ Farklı bir content type veya dynamic zone kullanmak için aşağıdan seçim yapın.
410
+ Ayarlarınız <strong>otomatik olarak kaydedilir</strong> ve Strapi yeniden başlatıldığında da geçerli olur.
411
+ Kod düzenlemeye gerek yok! ✨
412
+ </Typography>
413
+
414
+ {contentTypes.length === 0 ? (
415
+ <Alert variant="warning" title="Uyarı">
416
+ Dynamic zone içeren content type bulunamadı. Lütfen önce bir content type oluşturun.
417
+ </Alert>
418
+ ) : (
419
+ <Grid.Root gap={4}>
420
+ <Grid.Item col={5} s={12}>
421
+ <Box>
422
+ <Typography variant="sigma" textColor="neutral700" marginBottom={2}>
423
+ Content Type
424
+ </Typography>
425
+ <SingleSelect
426
+ value={selectedContentType}
427
+ onChange={val => {
428
+ setSelectedContentType(val);
429
+ // İlk dynamic zone'u otomatik seç
430
+ const ct = contentTypes.find(c => c.uid === val);
431
+ if (ct?.dynamicZones?.length > 0) {
432
+ setSelectedDynamicZone(ct.dynamicZones[0].name);
433
+ } else {
434
+ setSelectedDynamicZone('');
435
+ }
436
+ }}
437
+ placeholder="Content type seçin..."
438
+ >
439
+ {contentTypes.map(ct => (
440
+ <SingleSelectOption key={ct.uid} value={ct.uid}>
441
+ {ct.displayName} ({ct.kind === 'collectionType' ? 'Collection' : 'Single'})
442
+ </SingleSelectOption>
443
+ ))}
444
+ </SingleSelect>
445
+ </Box>
446
+ </Grid.Item>
447
+
448
+ <Grid.Item col={4} s={12}>
449
+ <Box>
450
+ <Typography variant="sigma" textColor="neutral700" marginBottom={2}>
451
+ Dynamic Zone Field
452
+ </Typography>
453
+ <SingleSelect
454
+ value={selectedDynamicZone}
455
+ onChange={setSelectedDynamicZone}
456
+ placeholder="Dynamic zone seçin..."
457
+ disabled={!selectedContentType}
458
+ >
459
+ {getAvailableDynamicZones().map(dz => (
460
+ <SingleSelectOption key={dz.name} value={dz.name}>
461
+ {dz.name} ({dz.components.length} component)
462
+ </SingleSelectOption>
463
+ ))}
464
+ </SingleSelect>
465
+ </Box>
466
+ </Grid.Item>
467
+
468
+ <Grid.Item col={3} s={12}>
469
+ <Box>
470
+ <Typography variant="sigma" textColor="neutral700" marginBottom={2}>
471
+ &nbsp;
472
+ </Typography>
473
+ <Button
474
+ onClick={handleConfigUpdate}
475
+ loading={configLoading}
476
+ disabled={!selectedContentType || !selectedDynamicZone ||
477
+ (selectedContentType === currentConfig.contentType &&
478
+ selectedDynamicZone === currentConfig.dynamicZoneField)}
479
+ fullWidth
480
+ >
481
+ 💾 Kaydet
482
+ </Button>
483
+ </Box>
484
+ </Grid.Item>
485
+ </Grid.Root>
486
+ )}
487
+
488
+ {contentTypes.length > 0 && (
489
+ <Box marginTop={4} padding={3} background="neutral0" hasRadius>
490
+ <Typography variant="sigma" textColor="neutral700" marginBottom={2}>
491
+ 💡 Mevcut Content Type'lar ve Dynamic Zone'ları:
492
+ </Typography>
493
+ <Box style={{ maxHeight: '150px', overflowY: 'auto' }}>
494
+ {contentTypes.map(ct => (
495
+ <Flex key={ct.uid} gap={2} marginBottom={1} alignItems="center">
496
+ <Badge variant={ct.uid === currentConfig.contentType ? "success" : "secondary"}>
497
+ {ct.displayName}
498
+ </Badge>
499
+ <Typography variant="omega" textColor="neutral600">
500
+ → {ct.dynamicZones.map(dz => dz.name).join(', ')}
501
+ </Typography>
502
+ </Flex>
503
+ ))}
504
+ </Box>
505
+ </Box>
506
+ )}
507
+ </Box>
508
+ )}
509
+
510
+ {message && (
511
+ <Box marginBottom={6}>
512
+ <Alert
513
+ closeLabel="Close"
514
+ title={message.type === "success" ? "Success" : "Error"}
515
+ variant={message.type === "success" ? "success" : "danger"}
516
+ onClose={() => {
517
+ setMessage(null);
518
+ setCopyDetails(null);
519
+ }}
520
+ onClick={() => {
521
+ if (message.type === "success" && copyDetails) {
522
+ setShowDetailsModal(true);
523
+ }
524
+ }}
525
+ style={{ cursor: message.type === "success" && copyDetails ? "pointer" : "default" }}
526
+ >
527
+ {message.text}
528
+ </Alert>
529
+ </Box>
530
+ )}
531
+
532
+ {showDetailsModal && copyDetails && (
533
+ <div
534
+ style={{
535
+ position: "fixed",
536
+ top: 0,
537
+ left: 0,
538
+ right: 0,
539
+ bottom: 0,
540
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
541
+ zIndex: 1000,
542
+ display: "flex",
543
+ alignItems: "center",
544
+ justifyContent: "center",
545
+ padding: "20px",
546
+ }}
547
+ onClick={() => setShowDetailsModal(false)}
548
+ >
549
+ <Card
550
+ style={{
551
+ maxWidth: "800px",
552
+ width: "100%",
553
+ maxHeight: "90vh",
554
+ display: "flex",
555
+ flexDirection: "column",
556
+ }}
557
+ onClick={(e) => e.stopPropagation()}
558
+ >
559
+ <Box
560
+ padding={4}
561
+ borderColor="neutral200"
562
+ style={{
563
+ borderBottom: "1px solid",
564
+ display: "flex",
565
+ justifyContent: "space-between",
566
+ alignItems: "center",
567
+ }}
568
+ >
569
+ <Typography fontWeight="bold" variant="beta" as="h2">
570
+ Copy Details
571
+ </Typography>
572
+ <IconButton
573
+ variant="ghost"
574
+ onClick={() => setShowDetailsModal(false)}
575
+ label="Close"
576
+ >
577
+ <Cross />
578
+ </IconButton>
579
+ </Box>
580
+ <Box
581
+ padding={4}
582
+ style={{
583
+ overflowY: "auto",
584
+ flex: 1,
585
+ }}
586
+ >
587
+ {copyDetails.map((detail, idx) => (
588
+ <Box key={idx} marginBottom={6}>
589
+ <Card>
590
+ <Box padding={4}>
591
+ <Flex justifyContent="space-between" alignItems="center" marginBottom={4}>
592
+ <Typography variant="beta" fontWeight="bold">
593
+ {detail.componentType.replace("sections.", "").replace(/-/g, " ").replace(/\b\w/g, l => l.toUpperCase())}
594
+ </Typography>
595
+ <Flex gap={2}>
596
+ <Badge>
597
+ {detail.totalFields} fields • {detail.totalMedia} media
598
+ </Badge>
599
+ {detail.totalRemoved > 0 && (
600
+ <Badge variant="secondary">
601
+ {detail.totalRemoved} system
602
+ </Badge>
603
+ )}
604
+ </Flex>
605
+ </Flex>
606
+
607
+ {detail.mediaFields.length > 0 && (
608
+ <Box marginBottom={4}>
609
+ <Typography variant="sigma" textColor="neutral700" marginBottom={2}>
610
+ 📷 Media Files ({detail.totalMedia}):
611
+ </Typography>
612
+ {detail.mediaFields.map((media, mIdx) => (
613
+ <Box key={mIdx} padding={3} marginBottom={2} background="neutral100" hasRadius>
614
+ <Typography variant="omega" fontWeight="bold" marginBottom={1}>
615
+ {media.path}:
616
+ </Typography>
617
+ {media.items.map((item, iIdx) => (
618
+ <Box key={iIdx} padding={2} marginTop={1} background="neutral0" hasRadius>
619
+ <Flex alignItems="center" gap={2}>
620
+ <Typography variant="omega" textColor="success600">
621
+
622
+ </Typography>
623
+ <Box>
624
+ <Typography variant="omega" fontWeight="semiBold">
625
+ {item.name}
626
+ </Typography>
627
+ <Typography variant="omega" textColor="neutral600" fontSize={1}>
628
+ {item.mime} • ID: {item.id}
629
+ </Typography>
630
+ </Box>
631
+ </Flex>
632
+ </Box>
633
+ ))}
634
+ </Box>
635
+ ))}
636
+ </Box>
637
+ )}
638
+
639
+ {detail.fields.length > 0 && (
640
+ <Box marginBottom={4}>
641
+ <Typography variant="sigma" textColor="neutral700" marginBottom={2}>
642
+ 📝 Fields ({detail.totalFields}):
643
+ </Typography>
644
+ <Box style={{ maxHeight: "300px", overflowY: "auto" }}>
645
+ {detail.fields.slice(0, 20).map((field, fIdx) => (
646
+ <Box key={fIdx} padding={2} marginBottom={1} background="neutral0" hasRadius>
647
+ <Flex alignItems="center" gap={2}>
648
+ <Typography variant="omega" textColor="success600">
649
+
650
+ </Typography>
651
+ <Box style={{ flex: 1 }}>
652
+ <Typography variant="omega" fontWeight="semiBold">
653
+ {field.path}
654
+ </Typography>
655
+ <Typography variant="omega" textColor="neutral600" fontSize={1}>
656
+ Type: {field.type}
657
+ {field.value !== undefined && ` • Value: ${String(field.value).substring(0, 50)}`}
658
+ {field.count !== undefined && ` • Count: ${field.count}`}
659
+ </Typography>
660
+ </Box>
661
+ </Flex>
662
+ </Box>
663
+ ))}
664
+ {detail.fields.length > 20 && (
665
+ <Typography variant="omega" textColor="neutral600" marginTop={2}>
666
+ ... and {detail.fields.length - 20} more fields
667
+ </Typography>
668
+ )}
669
+ </Box>
670
+ </Box>
671
+ )}
672
+
673
+ {detail.removedFields && detail.removedFields.length > 0 && (
674
+ <Box>
675
+ <Flex alignItems="center" gap={2} marginBottom={2}>
676
+ <Typography variant="sigma" textColor="neutral600">
677
+ ℹ️ System Fields ({detail.totalRemoved}):
678
+ </Typography>
679
+ <Badge variant="secondary" size="S">Auto-handled</Badge>
680
+ </Flex>
681
+ <Box padding={3} background="neutral100" hasRadius marginBottom={2}>
682
+ <Typography variant="omega" textColor="neutral600" style={{ fontSize: '12px' }}>
683
+ 💡 Aşağıdaki alanlar otomatik olarak yönetiliyor. ID'ler silinir ve Strapi yeni benzersiz ID'ler oluşturur. Bu normal bir davranıştır ve veri kaybına neden olmaz.
684
+ </Typography>
685
+ </Box>
686
+ <Box style={{ maxHeight: "150px", overflowY: "auto" }}>
687
+ {detail.removedFields.map((field, rIdx) => (
688
+ <Box key={rIdx} padding={2} marginBottom={1} background="neutral100" hasRadius>
689
+ <Flex alignItems="center" gap={2}>
690
+ <Typography variant="omega" textColor="neutral500">
691
+
692
+ </Typography>
693
+ <Box style={{ flex: 1 }}>
694
+ <Typography variant="omega" fontWeight="semiBold" textColor="neutral700">
695
+ {field.path}
696
+ </Typography>
697
+ <Typography variant="omega" textColor="neutral500" style={{ fontSize: '11px' }}>
698
+ {field.reason === 'System field (automatically removed)'
699
+ ? 'Sistem alanı - Yeni ID atanacak'
700
+ : 'Nested ID - Otomatik oluşturulacak'}
701
+ </Typography>
702
+ </Box>
703
+ </Flex>
704
+ </Box>
705
+ ))}
706
+ </Box>
707
+ </Box>
708
+ )}
709
+
710
+ {detail.fields.length === 0 && detail.mediaFields.length === 0 && (
711
+ <Typography variant="omega" textColor="neutral600">
712
+ No fields detected in this component
713
+ </Typography>
714
+ )}
715
+ </Box>
716
+ </Card>
717
+ </Box>
718
+ ))}
719
+ </Box>
720
+ <Box
721
+ padding={4}
722
+ borderColor="neutral200"
723
+ style={{
724
+ borderTop: "1px solid",
725
+ display: "flex",
726
+ justifyContent: "flex-end",
727
+ }}
728
+ >
729
+ <Button onClick={() => setShowDetailsModal(false)} variant="tertiary">
730
+ Close
731
+ </Button>
732
+ </Box>
733
+ </Card>
734
+ </div>
735
+ )}
736
+
737
+ <Grid.Root gap={4}>
738
+ <Grid.Item col={6} xs={12}>
739
+ <Card style={{ height: "100%",width: "100%" }}>
740
+ <Box padding={4}>
741
+ <Typography variant="delta" tag="h2" marginBottom={4}>
742
+ Source Page
743
+ </Typography>
744
+
745
+ <Box marginBottom={4}>
746
+ <SingleSelect
747
+ label="Select source page"
748
+ placeholder="Choose page..."
749
+ value={sourcePageId}
750
+ onChange={setSourcePageId}
751
+ >
752
+ {pages.map((page) => {
753
+ const pageId = page.documentId || page.id;
754
+ return (
755
+ <SingleSelectOption key={pageId} value={pageId}>
756
+ {getPageTitle(page)}
757
+ </SingleSelectOption>
758
+ );
759
+ })}
760
+ </SingleSelect>
761
+ </Box>
762
+
763
+ <Box
764
+ style={{
765
+ minHeight: "400px",
766
+ maxHeight: "600px",
767
+ overflowY: "auto",
768
+ }}
769
+ padding={3}
770
+ background="neutral100"
771
+ hasRadius
772
+ >
773
+ {loadingSourceSections ? (
774
+ <Typography>Loading...</Typography>
775
+ ) : sourceSections.length > 0 ? (
776
+ <>
777
+ <Typography variant="sigma" textColor="neutral600" marginBottom={3}>
778
+ {sourceSections.length} SECTION(S) • DRAG TO COPY →
779
+ </Typography>
780
+ {sourceSections.map((section, index) => (
781
+ <Card
782
+ key={index}
783
+ style={{
784
+ cursor: "grab",
785
+ opacity: draggedIndex === index && draggedSource === 'source' ? 0.5 : 1,
786
+ marginBottom: 3,
787
+ }}
788
+ draggable
789
+ onDragStart={(e) => handleDragStart(e, index, 'source')}
790
+ onDragEnd={handleDragEnd}
791
+ >
792
+ <Box padding={3}>
793
+ <Flex justifyContent="space-between" alignItems="center">
794
+ <Box style={{ flex: 1 }}>
795
+ <Badge>
796
+ {getComponentName(section)}
797
+ </Badge>
798
+ <Typography variant="omega" marginTop={2} tag="p">
799
+ {getComponentPreview(section)}
800
+ </Typography>
801
+ </Box>
802
+ <IconButton
803
+ variant="ghost"
804
+ onClick={(e) => e.preventDefault()}
805
+ label="Drag"
806
+ noBorder
807
+ >
808
+ <Drag />
809
+ </IconButton>
810
+ </Flex>
811
+ </Box>
812
+ </Card>
813
+ ))}
814
+ </>
815
+ ) : sourcePageId ? (
816
+ <Typography textColor="neutral600">No sections in this page</Typography>
817
+ ) : (
818
+ <Typography textColor="neutral600">Select a source page</Typography>
819
+ )}
820
+ </Box>
821
+ </Box>
822
+ </Card>
823
+ </Grid.Item>
824
+
825
+ <Grid.Item col={6} xs={12}>
826
+ <Card style={{ height: "100%",width: "100%" }}>
827
+ <Box padding={4}>
828
+ <Flex justifyContent="space-between" alignItems="center" marginBottom={4}>
829
+ <Typography variant="delta" tag="h2">
830
+ Target Page
831
+ </Typography>
832
+ {hasUnsavedChanges && targetPageId && (
833
+ <Button
834
+ onClick={handlePublish}
835
+ loading={publishing}
836
+ variant="default"
837
+ size="S"
838
+ >
839
+ Publish
840
+ </Button>
841
+ )}
842
+ </Flex>
843
+
844
+ {hasUnsavedChanges && (
845
+ <Box marginBottom={4} padding={3} background="warning100" hasRadius>
846
+ <Typography variant="omega" textColor="warning700">
847
+ ⚠️ You have unsaved changes. Click Publish to save and publish.
848
+ </Typography>
849
+ </Box>
850
+ )}
851
+
852
+ <Box marginBottom={4}>
853
+ <SingleSelect
854
+ label="Select target page"
855
+ placeholder="Choose page..."
856
+ value={targetPageId}
857
+ onChange={setTargetPageId}
858
+ >
859
+ {pages.map((page) => {
860
+ const pageId = page.documentId || page.id;
861
+ return (
862
+ <SingleSelectOption key={pageId} value={pageId}>
863
+ {getPageTitle(page)}
864
+ </SingleSelectOption>
865
+ );
866
+ })}
867
+ </SingleSelect>
868
+ </Box>
869
+
870
+ <Box
871
+ padding={3}
872
+ background={isDragOver ? "primary100" : "neutral100"}
873
+ hasRadius
874
+ style={{
875
+ minHeight: "400px",
876
+ maxHeight: "600px",
877
+ overflowY: "auto",
878
+ border: isDragOver ? "2px dashed" : "2px dashed transparent",
879
+ borderColor: isDragOver ? "primary500" : "transparent",
880
+ }}
881
+ onDragOver={(e) => {
882
+ if (draggedSource === 'source') {
883
+ handleDragOver(e);
884
+ }
885
+ }}
886
+ onDragLeave={handleDragLeave}
887
+ onDrop={(e) => {
888
+ if (draggedSource === 'source') {
889
+ handleDrop(e, targetSections.length);
890
+ }
891
+ }}
892
+ >
893
+ {loadingTargetSections ? (
894
+ <Typography>Loading...</Typography>
895
+ ) : (
896
+ <>
897
+ {targetSections.length > 0 && (
898
+ <Typography variant="sigma" textColor="neutral600" marginBottom={3}>
899
+ {targetSections.length} SECTION(S) • Drag to reorder
900
+ </Typography>
901
+ )}
902
+
903
+ {targetSections.map((section, index) => (
904
+ <React.Fragment key={index}>
905
+ {dragOverTargetIndex === index && (
906
+ <Box
907
+ style={{
908
+ height: "4px",
909
+ backgroundColor: "primary500",
910
+ marginBottom: 2,
911
+ borderRadius: "2px",
912
+ }}
913
+ />
914
+ )}
915
+ <Card
916
+ style={{
917
+ cursor: draggedSource === 'target' ? "move" : "default",
918
+ opacity: draggedIndex === index && draggedSource === 'target' ? 0.5 : 1,
919
+ marginBottom: 3,
920
+ }}
921
+ draggable={true}
922
+ onDragStart={(e) => handleDragStart(e, index, 'target')}
923
+ onDragEnd={handleDragEnd}
924
+ onDragOver={(e) => handleDragOver(e, index)}
925
+ onDragLeave={handleDragLeave}
926
+ onDrop={(e) => handleDrop(e, index)}
927
+ >
928
+ <Box padding={3}>
929
+ <Flex justifyContent="space-between" alignItems="flex-start">
930
+ <Box style={{ flex: 1 }}>
931
+ <Badge variant="secondary">
932
+ {getComponentName(section)}
933
+ </Badge>
934
+ <Typography variant="omega" marginTop={2} tag="p">
935
+ {getComponentPreview(section)}
936
+ </Typography>
937
+ </Box>
938
+ <IconButton
939
+ variant="ghost"
940
+ onClick={(e) => e.preventDefault()}
941
+ label="Drag"
942
+ noBorder
943
+ >
944
+ <Drag />
945
+ </IconButton>
946
+ </Flex>
947
+ </Box>
948
+ </Card>
949
+ </React.Fragment>
950
+ ))}
951
+
952
+ {dragOverTargetIndex === targetSections.length && (
953
+ <Box
954
+ style={{
955
+ height: "4px",
956
+ backgroundColor: "primary500",
957
+ marginTop: 2,
958
+ borderRadius: "2px",
959
+ }}
960
+ />
961
+ )}
962
+
963
+ {isDragOver && targetSections.length === 0 && (
964
+ <Box padding={8} textAlign="center">
965
+ <Typography variant="omega" textColor="primary600">
966
+ ↓ Drop here to copy section ↓
967
+ </Typography>
968
+ </Box>
969
+ )}
970
+
971
+ {!isDragOver && targetSections.length === 0 && targetPageId && (
972
+ <Box padding={8} textAlign="center">
973
+ <Typography variant="omega" textColor="neutral600">
974
+ No sections yet. Drag from source to add.
975
+ </Typography>
976
+ </Box>
977
+ )}
978
+
979
+ {!targetPageId && (
980
+ <Typography textColor="neutral600">Select a target page</Typography>
981
+ )}
982
+ </>
983
+ )}
984
+ </Box>
985
+ </Box>
986
+ </Card>
987
+ </Grid.Item>
988
+ </Grid.Root>
989
+ </Box>
990
+ </Main>
991
+ </Page.Main>
992
+ );
993
+ };
994
+
995
+ export default HomePage;