@checkstack/ui 0.0.2

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.
Files changed (68) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/bunfig.toml +2 -0
  3. package/package.json +40 -0
  4. package/src/components/Accordion.tsx +55 -0
  5. package/src/components/Alert.tsx +90 -0
  6. package/src/components/AmbientBackground.tsx +105 -0
  7. package/src/components/AnimatedCounter.tsx +54 -0
  8. package/src/components/BackLink.tsx +56 -0
  9. package/src/components/Badge.tsx +38 -0
  10. package/src/components/Button.tsx +55 -0
  11. package/src/components/Card.tsx +56 -0
  12. package/src/components/Checkbox.tsx +46 -0
  13. package/src/components/ColorPicker.tsx +69 -0
  14. package/src/components/CommandPalette.tsx +74 -0
  15. package/src/components/ConfirmationModal.tsx +134 -0
  16. package/src/components/DateRangeFilter.tsx +128 -0
  17. package/src/components/DateTimePicker.tsx +65 -0
  18. package/src/components/Dialog.tsx +134 -0
  19. package/src/components/DropdownMenu.tsx +96 -0
  20. package/src/components/DynamicForm/DynamicForm.tsx +126 -0
  21. package/src/components/DynamicForm/DynamicOptionsField.tsx +220 -0
  22. package/src/components/DynamicForm/FormField.tsx +690 -0
  23. package/src/components/DynamicForm/JsonField.tsx +98 -0
  24. package/src/components/DynamicForm/index.ts +11 -0
  25. package/src/components/DynamicForm/types.ts +95 -0
  26. package/src/components/DynamicForm/utils.ts +39 -0
  27. package/src/components/DynamicIcon.tsx +45 -0
  28. package/src/components/EditableText.tsx +141 -0
  29. package/src/components/EmptyState.tsx +32 -0
  30. package/src/components/HealthBadge.tsx +57 -0
  31. package/src/components/InfoBanner.tsx +97 -0
  32. package/src/components/Input.tsx +20 -0
  33. package/src/components/Label.tsx +17 -0
  34. package/src/components/LoadingSpinner.tsx +29 -0
  35. package/src/components/Markdown.tsx +206 -0
  36. package/src/components/NavItem.tsx +112 -0
  37. package/src/components/Page.tsx +58 -0
  38. package/src/components/PageLayout.tsx +83 -0
  39. package/src/components/PaginatedList.tsx +135 -0
  40. package/src/components/Pagination.tsx +195 -0
  41. package/src/components/PermissionDenied.tsx +31 -0
  42. package/src/components/PermissionGate.tsx +97 -0
  43. package/src/components/PluginConfigForm.tsx +91 -0
  44. package/src/components/SectionHeader.tsx +30 -0
  45. package/src/components/Select.tsx +157 -0
  46. package/src/components/StatusCard.tsx +78 -0
  47. package/src/components/StatusUpdateTimeline.tsx +222 -0
  48. package/src/components/StrategyConfigCard.tsx +333 -0
  49. package/src/components/SubscribeButton.tsx +96 -0
  50. package/src/components/Table.tsx +119 -0
  51. package/src/components/Tabs.tsx +141 -0
  52. package/src/components/TemplateEditor.test.ts +156 -0
  53. package/src/components/TemplateEditor.tsx +435 -0
  54. package/src/components/TerminalFeed.tsx +152 -0
  55. package/src/components/Textarea.tsx +22 -0
  56. package/src/components/ThemeProvider.tsx +76 -0
  57. package/src/components/Toast.tsx +118 -0
  58. package/src/components/ToastProvider.tsx +126 -0
  59. package/src/components/Toggle.tsx +47 -0
  60. package/src/components/Tooltip.tsx +20 -0
  61. package/src/components/UserMenu.tsx +79 -0
  62. package/src/hooks/usePagination.e2e.ts +275 -0
  63. package/src/hooks/usePagination.ts +231 -0
  64. package/src/index.ts +53 -0
  65. package/src/themes.css +204 -0
  66. package/src/utils/strip-markdown.ts +44 -0
  67. package/src/utils.ts +8 -0
  68. package/tsconfig.json +6 -0
@@ -0,0 +1,56 @@
1
+ import * as React from "react";
2
+ import { cn } from "../utils";
3
+
4
+ export const Card = ({
5
+ className,
6
+ ...props
7
+ }: React.HTMLAttributes<HTMLDivElement>) => (
8
+ <div
9
+ className={cn(
10
+ "rounded-lg border border-border bg-card text-card-foreground shadow-sm",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+
17
+ export const CardHeader = ({
18
+ className,
19
+ ...props
20
+ }: React.HTMLAttributes<HTMLDivElement>) => (
21
+ <div className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
22
+ );
23
+
24
+ export const CardTitle = ({
25
+ className,
26
+ ...props
27
+ }: React.HTMLAttributes<HTMLHeadingElement>) => (
28
+ <h3
29
+ className={cn(
30
+ "text-2xl font-semibold leading-none tracking-tight",
31
+ className
32
+ )}
33
+ {...props}
34
+ />
35
+ );
36
+
37
+ export const CardDescription = ({
38
+ className,
39
+ ...props
40
+ }: React.HTMLAttributes<HTMLParagraphElement>) => (
41
+ <p className={cn("text-sm text-muted-foreground", className)} {...props} />
42
+ );
43
+
44
+ export const CardContent = ({
45
+ className,
46
+ ...props
47
+ }: React.HTMLAttributes<HTMLDivElement>) => (
48
+ <div className={cn("p-6 pt-0", className)} {...props} />
49
+ );
50
+
51
+ export const CardFooter = ({
52
+ className,
53
+ ...props
54
+ }: React.HTMLAttributes<HTMLDivElement>) => (
55
+ <div className={cn("flex items-center p-6 pt-0", className)} {...props} />
56
+ );
@@ -0,0 +1,46 @@
1
+ import React from "react";
2
+ import { Check } from "lucide-react";
3
+ import { cn } from "../utils";
4
+
5
+ interface CheckboxProps extends React.InputHTMLAttributes<HTMLInputElement> {
6
+ onCheckedChange?: (checked: boolean) => void;
7
+ }
8
+
9
+ export const Checkbox: React.FC<CheckboxProps> = ({
10
+ className,
11
+ checked,
12
+ onCheckedChange,
13
+ ...props
14
+ }) => {
15
+ // Compute styles to avoid nested ternary
16
+ const getBackgroundStyles = () => {
17
+ if (props.disabled) {
18
+ return "bg-muted border-border cursor-not-allowed";
19
+ }
20
+ if (checked) {
21
+ return "bg-primary border-primary cursor-pointer";
22
+ }
23
+ return "bg-background border-input cursor-pointer";
24
+ };
25
+
26
+ return (
27
+ <div
28
+ className={cn(
29
+ "peer h-4 w-4 shrink-0 rounded-sm border ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex items-center justify-center transition-colors",
30
+ getBackgroundStyles(),
31
+ className
32
+ )}
33
+ onClick={() => !props.disabled && onCheckedChange?.(!checked)}
34
+ >
35
+ {checked && (
36
+ <Check
37
+ className={cn(
38
+ "h-3 w-3",
39
+ props.disabled ? "text-muted-foreground" : "text-primary-foreground"
40
+ )}
41
+ strokeWidth={3}
42
+ />
43
+ )}
44
+ </div>
45
+ );
46
+ };
@@ -0,0 +1,69 @@
1
+ import * as React from "react";
2
+ import { cn } from "../utils";
3
+
4
+ interface ColorPickerProps {
5
+ id?: string;
6
+ value: string;
7
+ onChange: (value: string) => void;
8
+ placeholder?: string;
9
+ className?: string;
10
+ }
11
+
12
+ export const ColorPicker: React.FC<ColorPickerProps> = ({
13
+ id,
14
+ value,
15
+ onChange,
16
+ placeholder = "#000000",
17
+ className,
18
+ }) => {
19
+ const colorInputRef = React.useRef<HTMLInputElement>(null);
20
+
21
+ // Normalize hex color (add # if missing, ensure valid format for color input)
22
+ const normalizedValue = React.useMemo(() => {
23
+ if (!value) return "#000000";
24
+ const hex = value.startsWith("#") ? value : `#${value}`;
25
+ // Validate hex format for the color input (must be #RRGGBB)
26
+ if (/^#[0-9A-Fa-f]{6}$/.test(hex)) {
27
+ return hex;
28
+ }
29
+ // Handle 3-character hex
30
+ if (/^#[0-9A-Fa-f]{3}$/.test(hex)) {
31
+ const r = hex[1];
32
+ const g = hex[2];
33
+ const b = hex[3];
34
+ return `#${r}${r}${g}${g}${b}${b}`;
35
+ }
36
+ return "#000000";
37
+ }, [value]);
38
+
39
+ return (
40
+ <div className={cn("flex items-center gap-2", className)}>
41
+ <button
42
+ type="button"
43
+ className="h-10 w-10 shrink-0 rounded-md border border-input overflow-hidden focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 transition-shadow"
44
+ style={{ backgroundColor: normalizedValue }}
45
+ onClick={() => colorInputRef.current?.click()}
46
+ aria-label="Pick color"
47
+ >
48
+ <input
49
+ ref={colorInputRef}
50
+ type="color"
51
+ value={normalizedValue}
52
+ onChange={(e) => onChange(e.target.value)}
53
+ className="sr-only"
54
+ tabIndex={-1}
55
+ />
56
+ </button>
57
+ <input
58
+ id={id}
59
+ type="text"
60
+ value={value || ""}
61
+ onChange={(e) => onChange(e.target.value)}
62
+ placeholder={placeholder}
63
+ className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm font-mono placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent disabled:cursor-not-allowed disabled:opacity-50"
64
+ />
65
+ </div>
66
+ );
67
+ };
68
+
69
+ ColorPicker.displayName = "ColorPicker";
@@ -0,0 +1,74 @@
1
+ import React from "react";
2
+ import { cn } from "../utils";
3
+ import { Command } from "lucide-react";
4
+
5
+ interface CommandPaletteProps {
6
+ onClick?: () => void;
7
+ placeholder?: string;
8
+ className?: string;
9
+ }
10
+
11
+ /**
12
+ * CommandPalette - Hero search bar with keyboard shortcut hint
13
+ * Displays a prominent search input that triggers a search dialog on click
14
+ */
15
+ export const CommandPalette: React.FC<CommandPaletteProps> = ({
16
+ onClick,
17
+ placeholder = "Search systems, incidents, or run commands...",
18
+ className,
19
+ }) => {
20
+ return (
21
+ <button
22
+ onClick={onClick}
23
+ className={cn(
24
+ // Base styles
25
+ "w-full flex items-center gap-3 px-4 py-3 rounded-xl",
26
+ // Glassmorphism effect
27
+ "bg-card/50 backdrop-blur-sm border border-border/50",
28
+ // Glow and shadow
29
+ "shadow-lg shadow-primary/5 hover:shadow-xl hover:shadow-primary/10",
30
+ // Hover state
31
+ "hover:border-primary/30 hover:bg-card/70",
32
+ // Transition
33
+ "transition-all duration-300 ease-out",
34
+ // Focus ring
35
+ "focus:outline-none focus:ring-2 focus:ring-primary/50 focus:ring-offset-2 focus:ring-offset-background",
36
+ // Cursor
37
+ "cursor-text",
38
+ className
39
+ )}
40
+ >
41
+ {/* Search icon */}
42
+ <svg
43
+ className="w-5 h-5 text-muted-foreground flex-shrink-0"
44
+ fill="none"
45
+ stroke="currentColor"
46
+ viewBox="0 0 24 24"
47
+ >
48
+ <path
49
+ strokeLinecap="round"
50
+ strokeLinejoin="round"
51
+ strokeWidth={2}
52
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
53
+ />
54
+ </svg>
55
+
56
+ {/* Placeholder text with typewriter effect */}
57
+ <span className="flex-1 text-left text-muted-foreground text-sm font-medium truncate">
58
+ {placeholder}
59
+ </span>
60
+
61
+ {/* Keyboard shortcut badge */}
62
+ <kbd
63
+ className={cn(
64
+ "hidden sm:flex items-center gap-1 px-2 py-1 rounded-md",
65
+ "bg-muted/50 border border-border/50",
66
+ "text-xs text-muted-foreground font-mono"
67
+ )}
68
+ >
69
+ <Command className="w-3 h-3" />
70
+ <span>K</span>
71
+ </kbd>
72
+ </button>
73
+ );
74
+ };
@@ -0,0 +1,134 @@
1
+ import React from "react";
2
+ import { cn } from "../utils";
3
+ import { Button } from "./Button";
4
+ import { AlertTriangle, X } from "lucide-react";
5
+
6
+ export interface ConfirmationModalProps {
7
+ isOpen: boolean;
8
+ onClose: () => void;
9
+ onConfirm: () => void;
10
+ title: string;
11
+ message: string;
12
+ confirmText?: string;
13
+ cancelText?: string;
14
+ variant?: "danger" | "warning" | "info";
15
+ isLoading?: boolean;
16
+ }
17
+
18
+ export const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
19
+ isOpen,
20
+ onClose,
21
+ onConfirm,
22
+ title,
23
+ message,
24
+ confirmText = "Confirm",
25
+ cancelText = "Cancel",
26
+ variant = "danger",
27
+ isLoading = false,
28
+ }) => {
29
+ if (!isOpen) return;
30
+
31
+ const handleConfirm = () => {
32
+ onConfirm();
33
+ };
34
+
35
+ const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
36
+ if (e.target === e.currentTarget) {
37
+ onClose();
38
+ }
39
+ };
40
+
41
+ const variantStyles = {
42
+ danger: {
43
+ icon: "text-destructive",
44
+ iconBg: "bg-destructive/10",
45
+ button:
46
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
47
+ },
48
+ warning: {
49
+ icon: "text-warning",
50
+ iconBg: "bg-warning/10",
51
+ button: "bg-warning text-warning-foreground hover:bg-warning/90",
52
+ },
53
+ info: {
54
+ icon: "text-info",
55
+ iconBg: "bg-info/10",
56
+ button: "bg-info text-info-foreground hover:bg-info/90",
57
+ },
58
+ };
59
+
60
+ const styles = variantStyles[variant];
61
+
62
+ return (
63
+ <div
64
+ className="fixed inset-0 z-[60] !m-0 flex items-center justify-center bg-black/50 transition-opacity pointer-events-auto"
65
+ onClick={handleBackdropClick}
66
+ >
67
+ <div
68
+ className="bg-background rounded-lg shadow-xl max-w-md w-full mx-4 animate-in fade-in zoom-in duration-200 pointer-events-auto"
69
+ role="dialog"
70
+ aria-modal="true"
71
+ aria-labelledby="modal-title"
72
+ aria-describedby="modal-description"
73
+ onClick={(e) => e.stopPropagation()}
74
+ >
75
+ {/* Header */}
76
+ <div className="flex items-start justify-between p-6 pb-4">
77
+ <div className="flex items-start gap-4">
78
+ <div className={cn("rounded-full p-2", styles.iconBg)}>
79
+ <AlertTriangle className={cn("w-6 h-6", styles.icon)} />
80
+ </div>
81
+ <div className="flex-1">
82
+ <h3
83
+ id="modal-title"
84
+ className="text-lg font-semibold text-foreground"
85
+ >
86
+ {title}
87
+ </h3>
88
+ </div>
89
+ </div>
90
+ <button
91
+ onClick={onClose}
92
+ className="text-muted-foreground hover:text-foreground transition-colors"
93
+ disabled={isLoading}
94
+ >
95
+ <X className="w-5 h-5" />
96
+ </button>
97
+ </div>
98
+
99
+ {/* Body */}
100
+ <div className="px-6 pb-6">
101
+ <p
102
+ id="modal-description"
103
+ className="text-sm text-muted-foreground pl-14"
104
+ >
105
+ {message}
106
+ </p>
107
+ </div>
108
+
109
+ {/* Footer */}
110
+ <div className="flex items-center justify-end gap-3 px-6 py-4 bg-muted/30 rounded-b-lg">
111
+ <Button
112
+ variant="ghost"
113
+ onClick={onClose}
114
+ disabled={isLoading}
115
+ type="button"
116
+ >
117
+ {cancelText}
118
+ </Button>
119
+ <button
120
+ onClick={handleConfirm}
121
+ disabled={isLoading}
122
+ className={cn(
123
+ "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none h-10 px-4 py-2",
124
+ styles.button
125
+ )}
126
+ type="button"
127
+ >
128
+ {isLoading ? "Processing..." : confirmText}
129
+ </button>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ );
134
+ };
@@ -0,0 +1,128 @@
1
+ import React, { useState, useMemo } from "react";
2
+ import { subDays, subHours, startOfDay } from "date-fns";
3
+ import { DateTimePicker } from "./DateTimePicker";
4
+ import { Button } from "./Button";
5
+ import { Calendar } from "lucide-react";
6
+
7
+ export interface DateRange {
8
+ startDate: Date;
9
+ endDate: Date;
10
+ }
11
+
12
+ export type DateRangePreset = "24h" | "7d" | "30d" | "custom";
13
+
14
+ export interface DateRangeFilterProps {
15
+ value: DateRange;
16
+ onChange: (range: DateRange) => void;
17
+ className?: string;
18
+ }
19
+
20
+ const PRESETS: Array<{ id: DateRangePreset; label: string }> = [
21
+ { id: "24h", label: "Last 24h" },
22
+ { id: "7d", label: "Last 7 days" },
23
+ { id: "30d", label: "Last 30 days" },
24
+ { id: "custom", label: "Custom" },
25
+ ];
26
+
27
+ function getPresetRange(preset: DateRangePreset): DateRange {
28
+ const now = new Date();
29
+ switch (preset) {
30
+ case "24h": {
31
+ return { startDate: subHours(now, 24), endDate: now };
32
+ }
33
+ case "7d": {
34
+ return { startDate: startOfDay(subDays(now, 7)), endDate: now };
35
+ }
36
+ case "30d": {
37
+ return { startDate: startOfDay(subDays(now, 30)), endDate: now };
38
+ }
39
+ case "custom": {
40
+ return { startDate: startOfDay(subDays(now, 7)), endDate: now };
41
+ }
42
+ }
43
+ }
44
+
45
+ function detectPreset(range: DateRange): DateRangePreset {
46
+ const now = new Date();
47
+ const diffMs = now.getTime() - range.startDate.getTime();
48
+ const diffHours = diffMs / (1000 * 60 * 60);
49
+ const diffDays = diffHours / 24;
50
+
51
+ if (diffHours <= 25 && diffHours >= 23) return "24h";
52
+ if (diffDays <= 8 && diffDays >= 6) return "7d";
53
+ if (diffDays <= 31 && diffDays >= 29) return "30d";
54
+ return "custom";
55
+ }
56
+
57
+ /**
58
+ * Date range filter with preset buttons and custom date pickers.
59
+ */
60
+ export const DateRangeFilter: React.FC<DateRangeFilterProps> = ({
61
+ value,
62
+ onChange,
63
+ className,
64
+ }) => {
65
+ const activePreset = useMemo(() => detectPreset(value), [value]);
66
+ const [showCustom, setShowCustom] = useState(activePreset === "custom");
67
+
68
+ const handlePresetClick = (preset: DateRangePreset) => {
69
+ if (preset === "custom") {
70
+ setShowCustom(true);
71
+ } else {
72
+ setShowCustom(false);
73
+ onChange(getPresetRange(preset));
74
+ }
75
+ };
76
+
77
+ return (
78
+ <div className={`space-y-3 ${className ?? ""}`}>
79
+ <div className="flex items-center gap-2 flex-wrap">
80
+ <Calendar className="h-4 w-4 text-muted-foreground" />
81
+ <span className="text-sm font-medium text-muted-foreground">
82
+ Time range:
83
+ </span>
84
+ <div className="flex gap-1">
85
+ {PRESETS.map((preset) => (
86
+ <Button
87
+ key={preset.id}
88
+ variant={
89
+ activePreset === preset.id && !showCustom
90
+ ? "primary"
91
+ : preset.id === "custom" && showCustom
92
+ ? "primary"
93
+ : "outline"
94
+ }
95
+ size="sm"
96
+ onClick={() => handlePresetClick(preset.id)}
97
+ >
98
+ {preset.label}
99
+ </Button>
100
+ ))}
101
+ </div>
102
+ </div>
103
+
104
+ {showCustom && (
105
+ <div className="flex items-center gap-2 flex-wrap">
106
+ <span className="text-sm text-muted-foreground">From:</span>
107
+ <DateTimePicker
108
+ value={value.startDate}
109
+ onChange={(startDate) => onChange({ ...value, startDate })}
110
+ maxDate={value.endDate}
111
+ />
112
+ <span className="text-sm text-muted-foreground">To:</span>
113
+ <DateTimePicker
114
+ value={value.endDate}
115
+ onChange={(endDate) => onChange({ ...value, endDate })}
116
+ minDate={value.startDate}
117
+ maxDate={new Date()}
118
+ />
119
+ </div>
120
+ )}
121
+ </div>
122
+ );
123
+ };
124
+
125
+ /** Create a default date range (last 7 days) */
126
+ export function getDefaultDateRange(): DateRange {
127
+ return getPresetRange("7d");
128
+ }
@@ -0,0 +1,65 @@
1
+ import React from "react";
2
+ import { format } from "date-fns";
3
+ import { Input } from "./Input";
4
+ import { Calendar, Clock } from "lucide-react";
5
+
6
+ export interface DateTimePickerProps {
7
+ value: Date;
8
+ onChange: (date: Date) => void;
9
+ minDate?: Date;
10
+ maxDate?: Date;
11
+ className?: string;
12
+ }
13
+
14
+ /**
15
+ * Combined date and time picker component
16
+ */
17
+ export const DateTimePicker: React.FC<DateTimePickerProps> = ({
18
+ value,
19
+ onChange,
20
+ minDate,
21
+ maxDate,
22
+ className,
23
+ }) => {
24
+ const dateString = format(value, "yyyy-MM-dd");
25
+ const timeString = format(value, "HH:mm");
26
+
27
+ const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
28
+ const [year, month, day] = e.target.value.split("-").map(Number);
29
+ const newDate = new Date(value);
30
+ newDate.setFullYear(year, month - 1, day);
31
+ onChange(newDate);
32
+ };
33
+
34
+ const handleTimeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
35
+ const [hours, minutes] = e.target.value.split(":").map(Number);
36
+ const newDate = new Date(value);
37
+ newDate.setHours(hours, minutes);
38
+ onChange(newDate);
39
+ };
40
+
41
+ return (
42
+ <div className={`flex gap-2 ${className ?? ""}`}>
43
+ <div className="relative flex-1">
44
+ <Input
45
+ type="date"
46
+ value={dateString}
47
+ onChange={handleDateChange}
48
+ min={minDate ? format(minDate, "yyyy-MM-dd") : undefined}
49
+ max={maxDate ? format(maxDate, "yyyy-MM-dd") : undefined}
50
+ className="pl-9"
51
+ />
52
+ <Calendar className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
53
+ </div>
54
+ <div className="relative w-28">
55
+ <Input
56
+ type="time"
57
+ value={timeString}
58
+ onChange={handleTimeChange}
59
+ className="pl-9"
60
+ />
61
+ <Clock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
62
+ </div>
63
+ </div>
64
+ );
65
+ };