@weirdfingers/baseboards 0.4.1 → 0.5.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.
@@ -14,7 +14,7 @@
14
14
  "@radix-ui/react-navigation-menu": "^1.2.14",
15
15
  "@radix-ui/react-slot": "^1.2.3",
16
16
  "@tailwindcss/postcss": "^4.1.13",
17
- "@weirdfingers/boards": "^0.4.1",
17
+ "@weirdfingers/boards": "^0.5.0",
18
18
  "class-variance-authority": "^0.7.1",
19
19
  "clsx": "^2.0.0",
20
20
  "graphql": "^16.11.0",
@@ -2,32 +2,27 @@
2
2
 
3
3
  import React from "react";
4
4
  import { useParams } from "next/navigation";
5
- import { useBoard, useGenerators, useGeneration } from "@weirdfingers/boards";
5
+ import { useBoard, useGenerators, useGeneration, GeneratorSelectionProvider } from "@weirdfingers/boards";
6
6
  import { GenerationGrid } from "@/components/boards/GenerationGrid";
7
7
  import { GenerationInput } from "@/components/boards/GenerationInput";
8
8
 
9
9
  export default function BoardPage() {
10
10
  const params = useParams();
11
11
  const boardId = params.boardId as string;
12
- console.log("[BoardPage] Rendering with boardId:", boardId);
13
12
 
14
- const boardHookResult = useBoard(boardId);
15
- console.log("[BoardPage] useBoard result:", boardHookResult);
16
13
  const {
17
14
  board,
18
15
  loading: boardLoading,
19
16
  error: boardError,
20
17
  refresh: refreshBoard,
21
- } = boardHookResult;
18
+ } = useBoard(boardId);
22
19
 
23
20
  // Fetch available generators
24
- const generatorsHookResult = useGenerators();
25
- console.log("[BoardPage] useGenerators result:", generatorsHookResult);
26
21
  const {
27
22
  generators,
28
23
  loading: generatorsLoading,
29
24
  error: generatorsError,
30
- } = generatorsHookResult;
25
+ } = useGenerators();
31
26
 
32
27
  // Use generation hook for submitting generations and real-time progress
33
28
  const {
@@ -38,10 +33,6 @@ export default function BoardPage() {
38
33
  result,
39
34
  } = useGeneration();
40
35
 
41
- console.log("[BoardPage] board:", board);
42
- console.log("[BoardPage] boardError:", boardError);
43
- console.log("[BoardPage] board is null/undefined?", !board);
44
-
45
36
  // Refresh board when a generation completes or fails
46
37
  // MUST be before conditional returns to satisfy Rules of Hooks
47
38
  React.useEffect(() => {
@@ -49,10 +40,6 @@ export default function BoardPage() {
49
40
  progress &&
50
41
  (progress.status === "completed" || progress.status === "failed")
51
42
  ) {
52
- console.log(
53
- "[BoardPage] Generation finished, refreshing board:",
54
- progress.status
55
- );
56
43
  refreshBoard();
57
44
  }
58
45
  }, [progress, refreshBoard]);
@@ -121,12 +108,6 @@ export default function BoardPage() {
121
108
 
122
109
  // Handle loading state
123
110
  if (boardLoading || !board) {
124
- console.log(
125
- "[BoardPage] Showing loading spinner - boardLoading:",
126
- boardLoading,
127
- "board:",
128
- board
129
- );
130
111
  return (
131
112
  <div className="flex items-center justify-center min-h-screen">
132
113
  <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
@@ -134,8 +115,6 @@ export default function BoardPage() {
134
115
  );
135
116
  }
136
117
 
137
- console.log("[BoardPage] Board loaded successfully:", board);
138
-
139
118
  // Filter completed generations that can be used as inputs
140
119
  const availableArtifacts = generations.filter(
141
120
  (gen) => gen.status === "COMPLETED" && gen.storageUrl
@@ -186,47 +165,48 @@ export default function BoardPage() {
186
165
  };
187
166
 
188
167
  return (
189
- <main className="min-h-screen bg-gray-50">
190
- <div className="container mx-auto px-4 py-6 max-w-7xl">
191
- {/* Header */}
192
- <div className="mb-6">
193
- <h1 className="text-3xl font-bold text-gray-900">{board.title}</h1>
194
- {board.description && (
195
- <p className="text-gray-600 mt-2">{board.description}</p>
196
- )}
197
- </div>
198
-
199
- {/* Generation Grid */}
200
- <div className="mb-8">
201
- <GenerationGrid
202
- generations={generations}
203
- onGenerationClick={(gen) => {
204
- console.log("Clicked generation:", gen);
205
- // TODO: Open generation detail modal
206
- }}
207
- />
208
- </div>
209
-
210
- {/* Generation Input */}
211
- <div className="sticky bottom-6 z-10">
212
- {generatorsLoading ? (
213
- <div className="bg-white rounded-lg shadow-lg p-6 text-center">
214
- <p className="text-gray-500">Loading generators...</p>
215
- </div>
216
- ) : generators.length === 0 ? (
217
- <div className="bg-white rounded-lg shadow-lg p-6 text-center">
218
- <p className="text-gray-500">No generators available</p>
219
- </div>
220
- ) : (
221
- <GenerationInput
222
- generators={generators}
223
- availableArtifacts={availableArtifacts}
224
- onSubmit={handleGenerationSubmit}
225
- isGenerating={isGenerating}
168
+ <GeneratorSelectionProvider>
169
+ <main className="min-h-screen bg-gray-50">
170
+ <div className="container mx-auto px-4 py-6 max-w-7xl">
171
+ {/* Header */}
172
+ <div className="mb-6">
173
+ <h1 className="text-3xl font-bold text-gray-900">{board.title}</h1>
174
+ {board.description && (
175
+ <p className="text-gray-600 mt-2">{board.description}</p>
176
+ )}
177
+ </div>
178
+
179
+ {/* Generation Grid */}
180
+ <div className="mb-8">
181
+ <GenerationGrid
182
+ generations={generations}
183
+ onGenerationClick={() => {
184
+ // TODO: Open generation detail modal
185
+ }}
226
186
  />
227
- )}
187
+ </div>
188
+
189
+ {/* Generation Input */}
190
+ <div id="generation-input" className="sticky bottom-6 z-10">
191
+ {generatorsLoading ? (
192
+ <div className="bg-white rounded-lg shadow-lg p-6 text-center">
193
+ <p className="text-gray-500">Loading generators...</p>
194
+ </div>
195
+ ) : generators.length === 0 ? (
196
+ <div className="bg-white rounded-lg shadow-lg p-6 text-center">
197
+ <p className="text-gray-500">No generators available</p>
198
+ </div>
199
+ ) : (
200
+ <GenerationInput
201
+ generators={generators}
202
+ availableArtifacts={availableArtifacts}
203
+ onSubmit={handleGenerationSubmit}
204
+ isGenerating={isGenerating}
205
+ />
206
+ )}
207
+ </div>
228
208
  </div>
229
- </div>
230
- </main>
209
+ </main>
210
+ </GeneratorSelectionProvider>
231
211
  );
232
212
  }
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import React, { useState } from "react";
3
4
  import { FileVideo, Volume2, X } from "lucide-react";
4
5
  import Image from "next/image";
5
6
 
@@ -29,6 +30,8 @@ export function ArtifactInputSlots({
29
30
  availableArtifacts,
30
31
  onSelectArtifact,
31
32
  }: ArtifactInputSlotsProps) {
33
+ const [dragOverSlot, setDragOverSlot] = useState<string | null>(null);
34
+
32
35
  const getIcon = (type: string) => {
33
36
  switch (type.toLowerCase()) {
34
37
  case "video":
@@ -47,6 +50,56 @@ export function ArtifactInputSlots({
47
50
  );
48
51
  };
49
52
 
53
+ const handleDragOver = (
54
+ e: React.DragEvent,
55
+ slotType: string,
56
+ slotName: string
57
+ ) => {
58
+ e.preventDefault();
59
+ e.stopPropagation();
60
+
61
+ // Check if the dragged artifact type matches the slot type
62
+ try {
63
+ const data = e.dataTransfer.types.includes("application/json");
64
+ if (data) {
65
+ e.dataTransfer.dropEffect = "copy";
66
+ setDragOverSlot(slotName);
67
+ }
68
+ } catch (err) {
69
+ // Ignore errors during drag over
70
+ }
71
+ };
72
+
73
+ const handleDragLeave = (e: React.DragEvent) => {
74
+ e.preventDefault();
75
+ e.stopPropagation();
76
+ setDragOverSlot(null);
77
+ };
78
+
79
+ const handleDrop = (
80
+ e: React.DragEvent,
81
+ slotType: string,
82
+ slotName: string
83
+ ) => {
84
+ e.preventDefault();
85
+ e.stopPropagation();
86
+ setDragOverSlot(null);
87
+
88
+ try {
89
+ const jsonData = e.dataTransfer.getData("application/json");
90
+ if (jsonData) {
91
+ const artifact = JSON.parse(jsonData) as Generation;
92
+
93
+ // Check if artifact type matches slot type
94
+ if (artifact.artifactType.toLowerCase() === slotType.toLowerCase()) {
95
+ onSelectArtifact(slotName, artifact);
96
+ }
97
+ }
98
+ } catch (err) {
99
+ console.error("Error handling drop:", err);
100
+ }
101
+ };
102
+
50
103
  return (
51
104
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
52
105
  {slots.map((slot) => {
@@ -100,11 +153,22 @@ export function ArtifactInputSlots({
100
153
  </div>
101
154
  ) : (
102
155
  // Show slot placeholder
103
- <div className="border-2 border-dashed border-gray-300 rounded-lg p-6 hover:border-gray-400 transition-colors">
156
+ <div
157
+ className={`border-2 border-dashed rounded-lg p-6 transition-all ${
158
+ dragOverSlot === slot.name
159
+ ? "border-orange-500 bg-orange-50"
160
+ : "border-gray-300 hover:border-gray-400"
161
+ }`}
162
+ onDragOver={(e) => handleDragOver(e, slot.type, slot.name)}
163
+ onDragLeave={handleDragLeave}
164
+ onDrop={(e) => handleDrop(e, slot.type, slot.name)}
165
+ >
104
166
  <div className="flex flex-col items-center justify-center text-center">
105
167
  <div className="mb-2">{getIcon(slot.type)}</div>
106
168
  <p className="text-sm font-medium text-gray-700 mb-1">
107
- Add a {slot.type}
169
+ {dragOverSlot === slot.name
170
+ ? `Drop ${slot.type} here`
171
+ : `Add a ${slot.type}`}
108
172
  </p>
109
173
  {matchingArtifacts.length > 0 ? (
110
174
  <select
@@ -128,7 +192,7 @@ export function ArtifactInputSlots({
128
192
  </select>
129
193
  ) : (
130
194
  <p className="text-xs text-gray-500 mt-1">
131
- No {slot.type} artifacts in this board yet
195
+ No {slot.type} artifacts in this board yet.
132
196
  </p>
133
197
  )}
134
198
  </div>
@@ -1,5 +1,25 @@
1
- import { FileVideo, Volume2, FileText, Image as ImageIcon } from "lucide-react";
1
+ import React from "react";
2
+ import {
3
+ FileVideo,
4
+ Volume2,
5
+ FileText,
6
+ Image as ImageIcon,
7
+ Plus,
8
+ MoreVertical,
9
+ GripVertical,
10
+ Download,
11
+ Eye,
12
+ Play,
13
+ Pause,
14
+ RotateCcw,
15
+ } from "lucide-react";
2
16
  import Image from "next/image";
17
+ import {
18
+ DropdownMenu,
19
+ DropdownMenuContent,
20
+ DropdownMenuItem,
21
+ DropdownMenuTrigger,
22
+ } from "@/components/ui/dropdown-menu";
3
23
 
4
24
  interface ArtifactPreviewProps {
5
25
  artifactType: string;
@@ -8,6 +28,12 @@ interface ArtifactPreviewProps {
8
28
  status: string;
9
29
  errorMessage?: string | null;
10
30
  onClick?: () => void;
31
+ onAddToSlot?: () => void;
32
+ canAddToSlot?: boolean;
33
+ onDownload?: () => void;
34
+ onPreview?: () => void;
35
+ artifactId?: string;
36
+ prompt?: string | null;
11
37
  }
12
38
 
13
39
  export function ArtifactPreview({
@@ -17,13 +43,67 @@ export function ArtifactPreview({
17
43
  status,
18
44
  errorMessage,
19
45
  onClick,
46
+ onAddToSlot,
47
+ canAddToSlot = false,
48
+ onDownload,
49
+ onPreview,
50
+ artifactId,
51
+ prompt,
20
52
  }: ArtifactPreviewProps) {
53
+ const [isPlaying, setIsPlaying] = React.useState(false);
54
+ const [currentTime, setCurrentTime] = React.useState(0);
55
+ const [duration, setDuration] = React.useState(0);
56
+ const audioRef = React.useRef<HTMLAudioElement>(null);
57
+ const videoRef = React.useRef<HTMLVideoElement>(null);
58
+
21
59
  const isLoading = status === "PENDING" || status === "PROCESSING";
22
60
  const isFailed = status === "FAILED" || status === "CANCELLED";
61
+ const isComplete = status === "COMPLETED";
23
62
 
24
63
  // Determine which URL to use for preview
25
64
  const previewUrl = thumbnailUrl || storageUrl;
26
65
 
66
+ // Media control functions
67
+ const handlePlayPause = (e: React.MouseEvent) => {
68
+ e.stopPropagation();
69
+ const mediaElement = artifactType === "AUDIO" ? audioRef.current : videoRef.current;
70
+ if (!mediaElement) return;
71
+
72
+ if (isPlaying) {
73
+ mediaElement.pause();
74
+ setIsPlaying(false);
75
+ } else {
76
+ mediaElement.play();
77
+ setIsPlaying(true);
78
+ }
79
+ };
80
+
81
+ const handleRestart = (e: React.MouseEvent) => {
82
+ e.stopPropagation();
83
+ const mediaElement = artifactType === "AUDIO" ? audioRef.current : videoRef.current;
84
+ if (!mediaElement) return;
85
+
86
+ mediaElement.currentTime = 0;
87
+ setCurrentTime(0);
88
+ };
89
+
90
+ const handleTimeUpdate = () => {
91
+ const mediaElement = artifactType === "AUDIO" ? audioRef.current : videoRef.current;
92
+ if (!mediaElement) return;
93
+ setCurrentTime(mediaElement.currentTime);
94
+ };
95
+
96
+ const handleLoadedMetadata = () => {
97
+ const mediaElement = artifactType === "AUDIO" ? audioRef.current : videoRef.current;
98
+ if (!mediaElement) return;
99
+ setDuration(mediaElement.duration);
100
+ };
101
+
102
+ const handleMediaEnded = () => {
103
+ setIsPlaying(false);
104
+ setCurrentTime(0);
105
+ };
106
+
27
107
  const renderContent = () => {
28
108
  if (isFailed) {
29
109
  return (
@@ -66,28 +146,116 @@ export function ArtifactPreview({
66
146
  );
67
147
 
68
148
  case "VIDEO":
69
- return (
70
- <div className="relative w-full h-full">
71
- {previewUrl ? (
72
- <Image
73
- src={previewUrl}
74
- alt="Video thumbnail"
149
+ if (storageUrl) {
150
+ return (
151
+ <div className="relative w-full h-full">
152
+ <video
153
+ ref={videoRef}
154
+ src={storageUrl}
155
+ onTimeUpdate={handleTimeUpdate}
156
+ onLoadedMetadata={handleLoadedMetadata}
157
+ onEnded={handleMediaEnded}
158
+ preload="metadata"
75
159
  className="w-full h-full object-cover"
76
- width={512}
77
- height={512}
160
+ loop
78
161
  />
79
- ) : (
80
- <div className="flex items-center justify-center h-full bg-gray-100">
81
- <FileVideo className="w-12 h-12 text-gray-400" />
162
+ <div className="absolute top-2 right-2 z-10 rounded-full bg-black/50 p-1.5">
163
+ <FileVideo className="w-4 h-4 text-white" />
164
+ </div>
165
+ {/* Video playback controls overlay */}
166
+ <div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
167
+ <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 flex flex-col items-center gap-2 pointer-events-auto">
168
+ <div className="flex items-center gap-2">
169
+ <button
170
+ onClick={handlePlayPause}
171
+ className="p-3 rounded-full bg-white/20 hover:bg-white/30 text-white transition-colors"
172
+ >
173
+ {isPlaying ? (
174
+ <Pause className="w-6 h-6" />
175
+ ) : (
176
+ <Play className="w-6 h-6" />
177
+ )}
178
+ </button>
179
+ <button
180
+ onClick={handleRestart}
181
+ className="p-3 rounded-full bg-white/20 hover:bg-white/30 text-white transition-colors"
182
+ >
183
+ <RotateCcw className="w-6 h-6" />
184
+ </button>
185
+ </div>
186
+ {duration > 0 && (
187
+ <div className="text-sm text-white font-medium">
188
+ {Math.floor(currentTime)}s / {Math.floor(duration)}s
189
+ </div>
190
+ )}
191
+ </div>
82
192
  </div>
83
- )}
84
- <div className="absolute top-2 left-2 bg-black/50 rounded p-1">
85
- <FileVideo className="w-5 h-5 text-white" />
86
193
  </div>
194
+ );
195
+ }
196
+ return (
197
+ <div className="flex items-center justify-center h-full bg-gray-100">
198
+ <FileVideo className="w-12 h-12 text-gray-400" />
87
199
  </div>
88
200
  );
89
201
 
90
202
  case "AUDIO":
203
+ if (storageUrl) {
204
+ const truncatedPrompt = prompt
205
+ ? prompt.length > 60
206
+ ? prompt.substring(0, 60) + "..."
207
+ : prompt
208
+ : "Audio file";
209
+
210
+ return (
211
+ <div className="relative w-full h-full bg-gradient-to-br from-purple-500/10 to-blue-500/10">
212
+ <audio
213
+ ref={audioRef}
214
+ src={storageUrl}
215
+ onTimeUpdate={handleTimeUpdate}
216
+ onLoadedMetadata={handleLoadedMetadata}
217
+ onEnded={handleMediaEnded}
218
+ preload="metadata"
219
+ />
220
+
221
+ <div className="flex flex-col items-center justify-center p-4 h-full">
222
+ <Volume2 className="w-8 h-8 text-primary mb-2" />
223
+ <p className="text-xs text-center text-foreground leading-relaxed">
224
+ {truncatedPrompt}
225
+ </p>
226
+ </div>
227
+
228
+ {/* Audio playback controls overlay */}
229
+ <div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
230
+ <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 flex flex-col items-center gap-2 pointer-events-auto">
231
+ <div className="flex items-center gap-2">
232
+ <button
233
+ onClick={handlePlayPause}
234
+ className="p-3 rounded-full bg-white/20 hover:bg-white/30 text-white transition-colors"
235
+ >
236
+ {isPlaying ? (
237
+ <Pause className="w-6 h-6" />
238
+ ) : (
239
+ <Play className="w-6 h-6" />
240
+ )}
241
+ </button>
242
+ <button
243
+ onClick={handleRestart}
244
+ className="p-3 rounded-full bg-white/20 hover:bg-white/30 text-white transition-colors"
245
+ >
246
+ <RotateCcw className="w-6 h-6" />
247
+ </button>
248
+ </div>
249
+ {duration > 0 && (
250
+ <div className="text-sm text-white font-medium">
251
+ {Math.floor(currentTime)}s / {Math.floor(duration)}s
252
+ </div>
253
+ )}
254
+ </div>
255
+ </div>
256
+ </div>
257
+ );
258
+ }
91
259
  return (
92
260
  <div className="flex flex-col items-center justify-center h-full bg-gradient-to-br from-blue-900 to-blue-700">
93
261
  <Volume2 className="w-12 h-12 text-white mb-2" />
@@ -114,12 +282,116 @@ export function ArtifactPreview({
114
282
 
115
283
  return (
116
284
  <div
117
- className={`relative aspect-square rounded-lg overflow-hidden border border-gray-200 ${
118
- onClick ? "cursor-pointer hover:opacity-80 transition-opacity" : ""
119
- }`}
120
- onClick={onClick}
285
+ className="relative aspect-square rounded-lg overflow-hidden border border-gray-200 group"
286
+ draggable={isComplete && !!artifactId && canAddToSlot}
287
+ onDragStart={(e) => {
288
+ if (isComplete && artifactId) {
289
+ e.dataTransfer.setData(
290
+ "application/json",
291
+ JSON.stringify({
292
+ id: artifactId,
293
+ artifactType,
294
+ storageUrl,
295
+ thumbnailUrl,
296
+ })
297
+ );
298
+ e.dataTransfer.effectAllowed = "copy";
299
+ }
300
+ }}
121
301
  >
122
- {renderContent()}
302
+ <div
303
+ className={onClick ? "cursor-pointer aspect-square" : "aspect-square"}
304
+ onClick={onClick}
305
+ >
306
+ {renderContent()}
307
+ </div>
308
+
309
+ {/* Bottom overlay with controls - show for all artifacts when not loading/failed */}
310
+ {!isLoading && !isFailed && (
311
+ <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-3 opacity-0 group-hover:opacity-100 transition-opacity">
312
+ <div className="flex items-center justify-between gap-2">
313
+ {/* Drag handle - only for completed artifacts */}
314
+ {isComplete && (
315
+ <div
316
+ className="flex items-center gap-2 cursor-move text-white/80 hover:text-white"
317
+ title="Drag to input slot"
318
+ >
319
+ <GripVertical className="w-4 h-4" />
320
+ </div>
321
+ )}
322
+
323
+ <div
324
+ className={`flex items-center gap-2 ${
325
+ !isComplete ? "ml-auto" : ""
326
+ }`}
327
+ >
328
+ {/* Add button - only for completed artifacts */}
329
+ {isComplete && onAddToSlot && (
330
+ <button
331
+ onClick={(e) => {
332
+ e.stopPropagation();
333
+ onAddToSlot();
334
+ }}
335
+ disabled={!canAddToSlot}
336
+ className={`p-1.5 rounded transition-colors ${
337
+ canAddToSlot
338
+ ? "bg-white/20 hover:bg-white/30 text-white cursor-pointer"
339
+ : "bg-white/10 text-white/40 cursor-not-allowed"
340
+ }`}
341
+ title={
342
+ canAddToSlot
343
+ ? "Add to input slot"
344
+ : "No compatible input slots"
345
+ }
346
+ >
347
+ <Plus className="w-4 h-4" />
348
+ </button>
349
+ )}
350
+
351
+ {/* More options menu - show for all artifacts */}
352
+ {(onPreview || onDownload) && (
353
+ <DropdownMenu>
354
+ <DropdownMenuTrigger asChild>
355
+ <button
356
+ onClick={(e) => e.stopPropagation()}
357
+ className="p-1.5 rounded bg-white/20 hover:bg-white/30 text-white transition-colors"
358
+ title="More options"
359
+ >
360
+ <MoreVertical className="w-4 h-4" />
361
+ </button>
362
+ </DropdownMenuTrigger>
363
+ <DropdownMenuContent align="end" className="w-40">
364
+ {onPreview && (
365
+ <DropdownMenuItem
366
+ onClick={(e) => {
367
+ e.stopPropagation();
368
+ onPreview();
369
+ }}
370
+ className="cursor-pointer"
371
+ >
372
+ <Eye className="w-4 h-4 mr-2" />
373
+ Preview
374
+ </DropdownMenuItem>
375
+ )}
376
+ {onDownload && (
377
+ <DropdownMenuItem
378
+ onClick={(e) => {
379
+ e.stopPropagation();
380
+ onDownload();
381
+ }}
382
+ className="cursor-pointer"
383
+ >
384
+ <Download className="w-4 h-4 mr-2" />
385
+ Download
386
+ </DropdownMenuItem>
387
+ )}
388
+ </DropdownMenuContent>
389
+ </DropdownMenu>
390
+ )}
391
+ </div>
392
+ </div>
393
+ </div>
394
+ )}
123
395
  </div>
124
396
  );
125
397
  }