웹 타입에 맞는 형태로 보여지게 수정
This commit is contained in:
parent
6c45686157
commit
089249fb65
|
|
@ -0,0 +1,257 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { ComponentData, WebType } from "@/types/screen";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
// import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
// import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Database, Type, Hash, List, AlignLeft, CheckSquare, Radio, Calendar, Code, Building, File } from "lucide-react";
|
||||||
|
|
||||||
|
interface RealtimePreviewProps {
|
||||||
|
component: ComponentData;
|
||||||
|
isSelected?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
onDragStart?: (e: React.DragEvent) => void;
|
||||||
|
onDragEnd?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 웹 타입에 따른 위젯 렌더링
|
||||||
|
const renderWidget = (component: ComponentData) => {
|
||||||
|
const { widgetType, label, placeholder, required, readonly, columnName } = component;
|
||||||
|
|
||||||
|
// 디버깅: 실제 widgetType 값 확인
|
||||||
|
console.log('RealtimePreview - widgetType:', widgetType, 'columnName:', columnName);
|
||||||
|
|
||||||
|
const commonProps = {
|
||||||
|
placeholder: placeholder || `입력하세요...`,
|
||||||
|
disabled: readonly,
|
||||||
|
required: required,
|
||||||
|
className: "w-full",
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (widgetType) {
|
||||||
|
case "text":
|
||||||
|
case "email":
|
||||||
|
case "tel":
|
||||||
|
return <Input type={widgetType === "email" ? "email" : widgetType === "tel" ? "tel" : "text"} {...commonProps} />;
|
||||||
|
|
||||||
|
case "number":
|
||||||
|
case "decimal":
|
||||||
|
return <Input type="number" step={widgetType === "decimal" ? "0.01" : "1"} {...commonProps} />;
|
||||||
|
|
||||||
|
case "date":
|
||||||
|
case "datetime":
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type={widgetType === "datetime" ? "datetime-local" : "date"}
|
||||||
|
{...commonProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "select":
|
||||||
|
case "dropdown":
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
disabled={readonly}
|
||||||
|
required={required}
|
||||||
|
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<option value="">{placeholder || "선택하세요..."}</option>
|
||||||
|
<option value="option1">옵션 1</option>
|
||||||
|
<option value="option2">옵션 2</option>
|
||||||
|
<option value="option3">옵션 3</option>
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "textarea":
|
||||||
|
case "text_area":
|
||||||
|
return <Textarea {...commonProps} rows={3} />;
|
||||||
|
|
||||||
|
case "boolean":
|
||||||
|
case "checkbox":
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={`checkbox-${component.id}`}
|
||||||
|
disabled={readonly}
|
||||||
|
required={required}
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<Label htmlFor={`checkbox-${component.id}`} className="text-sm">
|
||||||
|
{label || columnName}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "radio":
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id={`radio1-${component.id}`}
|
||||||
|
name={`radio-${component.id}`}
|
||||||
|
disabled={readonly}
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<Label htmlFor={`radio1-${component.id}`} className="text-sm">
|
||||||
|
옵션 1
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id={`radio2-${component.id}`}
|
||||||
|
name={`radio-${component.id}`}
|
||||||
|
disabled={readonly}
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<Label htmlFor={`radio2-${component.id}`} className="text-sm">
|
||||||
|
옵션 2
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "code":
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
{...commonProps}
|
||||||
|
rows={4}
|
||||||
|
className="w-full font-mono text-sm"
|
||||||
|
placeholder="코드를 입력하세요..."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "entity":
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
disabled={readonly}
|
||||||
|
required={required}
|
||||||
|
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<option value="">엔티티를 선택하세요...</option>
|
||||||
|
<option value="user">사용자</option>
|
||||||
|
<option value="product">제품</option>
|
||||||
|
<option value="order">주문</option>
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "file":
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
disabled={readonly}
|
||||||
|
required={required}
|
||||||
|
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return <Input type="text" {...commonProps} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 위젯 타입 아이콘
|
||||||
|
const getWidgetIcon = (widgetType: WebType | undefined) => {
|
||||||
|
switch (widgetType) {
|
||||||
|
case "text":
|
||||||
|
case "email":
|
||||||
|
case "tel":
|
||||||
|
return <Type className="h-4 w-4 text-blue-600" />;
|
||||||
|
case "number":
|
||||||
|
case "decimal":
|
||||||
|
return <Hash className="h-4 w-4 text-green-600" />;
|
||||||
|
case "date":
|
||||||
|
case "datetime":
|
||||||
|
return <Calendar className="h-4 w-4 text-purple-600" />;
|
||||||
|
case "select":
|
||||||
|
case "dropdown":
|
||||||
|
return <List className="h-4 w-4 text-orange-600" />;
|
||||||
|
case "textarea":
|
||||||
|
case "text_area":
|
||||||
|
return <AlignLeft className="h-4 w-4 text-indigo-600" />;
|
||||||
|
case "boolean":
|
||||||
|
case "checkbox":
|
||||||
|
return <CheckSquare className="h-4 w-4 text-blue-600" />;
|
||||||
|
case "radio":
|
||||||
|
return <Radio className="h-4 w-4 text-blue-600" />;
|
||||||
|
case "code":
|
||||||
|
return <Code className="h-4 w-4 text-gray-600" />;
|
||||||
|
case "entity":
|
||||||
|
return <Building className="h-4 w-4 text-cyan-600" />;
|
||||||
|
case "file":
|
||||||
|
return <File className="h-4 w-4 text-yellow-600" />;
|
||||||
|
default:
|
||||||
|
return <Type className="h-4 w-4 text-gray-500" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RealtimePreview: React.FC<RealtimePreviewProps> = ({
|
||||||
|
component,
|
||||||
|
isSelected = false,
|
||||||
|
onClick,
|
||||||
|
onDragStart,
|
||||||
|
onDragEnd,
|
||||||
|
}) => {
|
||||||
|
const { type, label, tableName, columnName, widgetType, size, style } = component;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`absolute rounded border-2 transition-all ${
|
||||||
|
isSelected ? "border-blue-500 bg-blue-50 shadow-lg" : "border-gray-300 bg-white hover:border-gray-400"
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
left: `${component.position.x}px`,
|
||||||
|
top: `${component.position.y}px`,
|
||||||
|
width: `${size.width * 80 - 16}px`,
|
||||||
|
height: `${size.height}px`,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
onClick={onClick}
|
||||||
|
draggable
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
>
|
||||||
|
{type === "container" && (
|
||||||
|
<div className="flex h-full flex-col items-center justify-center p-4">
|
||||||
|
<div className="flex flex-col items-center space-y-2">
|
||||||
|
<Database className="h-8 w-8 text-blue-600" />
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-sm font-medium">{label}</div>
|
||||||
|
<div className="text-xs text-gray-500">{tableName}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === "widget" && (
|
||||||
|
<div className="flex h-full flex-col p-3">
|
||||||
|
{/* 위젯 헤더 */}
|
||||||
|
<div className="mb-2 flex items-center space-x-2">
|
||||||
|
{getWidgetIcon(widgetType)}
|
||||||
|
<div className="flex-1">
|
||||||
|
<Label className="text-sm font-medium">
|
||||||
|
{label || columnName}
|
||||||
|
{component.required && <span className="ml-1 text-red-500">*</span>}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 위젯 본체 */}
|
||||||
|
<div className="flex-1">{renderWidget(component)}</div>
|
||||||
|
|
||||||
|
{/* 위젯 정보 */}
|
||||||
|
<div className="mt-2 text-xs text-gray-500">
|
||||||
|
{columnName} ({widgetType})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RealtimePreview;
|
||||||
|
|
@ -25,9 +25,12 @@ import {
|
||||||
Table,
|
Table,
|
||||||
Settings,
|
Settings,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
Code,
|
||||||
|
Building,
|
||||||
|
File,
|
||||||
List,
|
List,
|
||||||
AlignLeft,
|
AlignLeft,
|
||||||
|
ChevronRight,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
ScreenDefinition,
|
ScreenDefinition,
|
||||||
|
|
@ -53,6 +56,7 @@ import StyleEditor from "./StyleEditor";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { RealtimePreview } from "./RealtimePreview";
|
||||||
|
|
||||||
interface ScreenDesignerProps {
|
interface ScreenDesignerProps {
|
||||||
selectedScreen: ScreenDefinition | null;
|
selectedScreen: ScreenDefinition | null;
|
||||||
|
|
@ -226,6 +230,30 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
dataType: "BOOLEAN",
|
dataType: "BOOLEAN",
|
||||||
isNullable: "NO",
|
isNullable: "NO",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
tableName: "user_info",
|
||||||
|
columnName: "profile_code",
|
||||||
|
columnLabel: "프로필 코드",
|
||||||
|
webType: "code",
|
||||||
|
dataType: "TEXT",
|
||||||
|
isNullable: "YES",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tableName: "user_info",
|
||||||
|
columnName: "department",
|
||||||
|
columnLabel: "부서",
|
||||||
|
webType: "entity",
|
||||||
|
dataType: "VARCHAR",
|
||||||
|
isNullable: "YES",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tableName: "user_info",
|
||||||
|
columnName: "profile_image",
|
||||||
|
columnLabel: "프로필 이미지",
|
||||||
|
webType: "file",
|
||||||
|
dataType: "VARCHAR",
|
||||||
|
isNullable: "YES",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -353,29 +381,44 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
|
|
||||||
// 웹타입에 따른 위젯 타입 매핑
|
// 웹타입에 따른 위젯 타입 매핑
|
||||||
const getWidgetTypeFromWebType = useCallback((webType: string): string => {
|
const getWidgetTypeFromWebType = useCallback((webType: string): string => {
|
||||||
|
console.log("getWidgetTypeFromWebType - input webType:", webType);
|
||||||
switch (webType) {
|
switch (webType) {
|
||||||
case "text":
|
case "text":
|
||||||
case "email":
|
|
||||||
case "tel":
|
|
||||||
return "text";
|
return "text";
|
||||||
|
case "email":
|
||||||
|
return "email";
|
||||||
|
case "tel":
|
||||||
|
return "tel";
|
||||||
case "number":
|
case "number":
|
||||||
case "decimal":
|
|
||||||
return "number";
|
return "number";
|
||||||
|
case "decimal":
|
||||||
|
return "decimal";
|
||||||
case "date":
|
case "date":
|
||||||
case "datetime":
|
|
||||||
return "date";
|
return "date";
|
||||||
|
case "datetime":
|
||||||
|
return "datetime";
|
||||||
case "select":
|
case "select":
|
||||||
case "dropdown":
|
|
||||||
return "select";
|
return "select";
|
||||||
|
case "dropdown":
|
||||||
|
return "dropdown";
|
||||||
case "textarea":
|
case "textarea":
|
||||||
case "text_area":
|
|
||||||
return "textarea";
|
return "textarea";
|
||||||
|
case "text_area":
|
||||||
|
return "text_area";
|
||||||
case "checkbox":
|
case "checkbox":
|
||||||
case "boolean":
|
|
||||||
return "checkbox";
|
return "checkbox";
|
||||||
|
case "boolean":
|
||||||
|
return "boolean";
|
||||||
case "radio":
|
case "radio":
|
||||||
return "radio";
|
return "radio";
|
||||||
|
case "code":
|
||||||
|
return "code";
|
||||||
|
case "entity":
|
||||||
|
return "entity";
|
||||||
|
case "file":
|
||||||
|
return "file";
|
||||||
default:
|
default:
|
||||||
|
console.log("getWidgetTypeFromWebType - default case, returning text for:", webType);
|
||||||
return "text";
|
return "text";
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -682,31 +725,41 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
className="flex cursor-pointer items-center space-x-2 p-2 pl-6 hover:bg-gray-100"
|
className="flex cursor-pointer items-center space-x-2 p-2 pl-6 hover:bg-gray-100"
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e) =>
|
onDragStart={(e) => {
|
||||||
|
console.log("Drag start - column:", column.columnName, "webType:", column.webType);
|
||||||
|
const widgetType = getWidgetTypeFromWebType(column.webType || "text");
|
||||||
|
console.log("Drag start - widgetType:", widgetType);
|
||||||
startDrag(
|
startDrag(
|
||||||
{
|
{
|
||||||
type: "widget",
|
type: "widget",
|
||||||
tableName: table.tableName,
|
tableName: table.tableName,
|
||||||
columnName: column.columnName,
|
columnName: column.columnName,
|
||||||
widgetType: getWidgetTypeFromWebType(column.webType || "text"),
|
widgetType: widgetType as WebType,
|
||||||
label: column.columnLabel || column.columnName,
|
label: column.columnLabel || column.columnName,
|
||||||
size: { width: 6, height: 60 },
|
size: { width: 6, height: 60 },
|
||||||
},
|
},
|
||||||
e,
|
e,
|
||||||
)
|
);
|
||||||
}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
{column.webType === "text" && <Type className="h-3 w-3 text-blue-600" />}
|
{column.webType === "text" && <Type className="h-3 w-3 text-blue-600" />}
|
||||||
|
{column.webType === "email" && <Type className="h-3 w-3 text-blue-600" />}
|
||||||
|
{column.webType === "tel" && <Type className="h-3 w-3 text-blue-600" />}
|
||||||
{column.webType === "number" && <Hash className="h-3 w-3 text-green-600" />}
|
{column.webType === "number" && <Hash className="h-3 w-3 text-green-600" />}
|
||||||
|
{column.webType === "decimal" && <Hash className="h-3 w-3 text-green-600" />}
|
||||||
{column.webType === "date" && <Calendar className="h-3 w-3 text-purple-600" />}
|
{column.webType === "date" && <Calendar className="h-3 w-3 text-purple-600" />}
|
||||||
|
{column.webType === "datetime" && <Calendar className="h-3 w-3 text-purple-600" />}
|
||||||
{column.webType === "select" && <List className="h-3 w-3 text-orange-600" />}
|
{column.webType === "select" && <List className="h-3 w-3 text-orange-600" />}
|
||||||
|
{column.webType === "dropdown" && <List className="h-3 w-3 text-orange-600" />}
|
||||||
{column.webType === "textarea" && <AlignLeft className="h-3 w-3 text-indigo-600" />}
|
{column.webType === "textarea" && <AlignLeft className="h-3 w-3 text-indigo-600" />}
|
||||||
|
{column.webType === "text_area" && <AlignLeft className="h-3 w-3 text-indigo-600" />}
|
||||||
{column.webType === "checkbox" && <CheckSquare className="h-3 w-3 text-blue-600" />}
|
{column.webType === "checkbox" && <CheckSquare className="h-3 w-3 text-blue-600" />}
|
||||||
|
{column.webType === "boolean" && <CheckSquare className="h-3 w-3 text-blue-600" />}
|
||||||
{column.webType === "radio" && <Radio className="h-3 w-3 text-blue-600" />}
|
{column.webType === "radio" && <Radio className="h-3 w-3 text-blue-600" />}
|
||||||
{!["text", "number", "date", "select", "textarea", "checkbox", "radio"].includes(
|
{column.webType === "code" && <Code className="h-3 w-3 text-gray-600" />}
|
||||||
column.webType,
|
{column.webType === "entity" && <Building className="h-3 w-3 text-cyan-600" />}
|
||||||
) && <Type className="h-3 w-3 text-blue-600" />}
|
{column.webType === "file" && <File className="h-3 w-3 text-yellow-600" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="text-sm font-medium">{column.columnLabel || column.columnName}</div>
|
<div className="text-sm font-medium">{column.columnLabel || column.columnName}</div>
|
||||||
|
|
@ -777,56 +830,16 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 컴포넌트들 */}
|
{/* 컴포넌트들 - 실시간 미리보기 */}
|
||||||
{layout.components.map((component) => (
|
{layout.components.map((component) => (
|
||||||
<div
|
<RealtimePreview
|
||||||
key={component.id}
|
key={component.id}
|
||||||
className={`absolute cursor-move rounded border-2 ${
|
component={component}
|
||||||
selectedComponent?.id === component.id
|
isSelected={selectedComponent?.id === component.id}
|
||||||
? "border-blue-500 bg-blue-50"
|
|
||||||
: "border-gray-300 bg-white hover:border-gray-400"
|
|
||||||
}`}
|
|
||||||
style={{
|
|
||||||
left: `${component.position.x}px`,
|
|
||||||
top: `${component.position.y}px`,
|
|
||||||
width: `${component.size.width * 80 - 16}px`,
|
|
||||||
height: `${component.size.height}px`,
|
|
||||||
}}
|
|
||||||
onClick={() => handleComponentClick(component)}
|
onClick={() => handleComponentClick(component)}
|
||||||
draggable
|
|
||||||
onDragStart={(e) => startComponentDrag(component, e)}
|
onDragStart={(e) => startComponentDrag(component, e)}
|
||||||
onDragEnd={endDrag}
|
onDragEnd={endDrag}
|
||||||
>
|
/>
|
||||||
<div className="flex h-full items-center justify-center p-2">
|
|
||||||
{component.type === "container" && (
|
|
||||||
<div className="flex flex-col items-center space-y-1">
|
|
||||||
<Database className="h-6 w-6 text-blue-600" />
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-sm font-medium">{component.label}</div>
|
|
||||||
<div className="text-xs text-gray-500">{component.tableName}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{component.type === "widget" && (
|
|
||||||
<div className="flex flex-col items-center space-y-1">
|
|
||||||
{component.widgetType === "text" && <Type className="h-6 w-6 text-blue-600" />}
|
|
||||||
{component.widgetType === "number" && <Hash className="h-6 w-6 text-green-600" />}
|
|
||||||
{component.widgetType === "date" && <Calendar className="h-6 w-6 text-purple-600" />}
|
|
||||||
{component.widgetType === "select" && <List className="h-6 w-6 text-orange-600" />}
|
|
||||||
{component.widgetType === "textarea" && <AlignLeft className="h-6 w-6 text-indigo-600" />}
|
|
||||||
{component.widgetType === "checkbox" && <CheckSquare className="h-6 w-6 text-blue-600" />}
|
|
||||||
{component.widgetType === "radio" && <Radio className="h-6 w-6 text-blue-600" />}
|
|
||||||
{!["text", "number", "date", "select", "textarea", "checkbox", "radio"].includes(
|
|
||||||
component.widgetType || "text",
|
|
||||||
) && <Type className="h-6 w-6 text-blue-600" />}
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-sm font-medium">{component.label}</div>
|
|
||||||
<div className="text-xs text-gray-500">{component.columnName}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
import { DayPicker } from "react-day-picker";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
|
||||||
|
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||||
|
|
||||||
|
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
|
||||||
|
return (
|
||||||
|
<DayPicker
|
||||||
|
showOutsideDays={showOutsideDays}
|
||||||
|
className={cn("p-3", className)}
|
||||||
|
classNames={{
|
||||||
|
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
||||||
|
month: "space-y-4",
|
||||||
|
caption: "flex justify-center pt-1 relative items-center",
|
||||||
|
caption_label: "text-sm font-medium",
|
||||||
|
nav: "space-x-1 flex items-center",
|
||||||
|
nav_button: cn(
|
||||||
|
buttonVariants({ variant: "outline" }),
|
||||||
|
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
||||||
|
),
|
||||||
|
nav_button_previous: "absolute left-1",
|
||||||
|
nav_button_next: "absolute right-1",
|
||||||
|
table: "w-full border-collapse space-y-1",
|
||||||
|
head_row: "flex",
|
||||||
|
head_cell: "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
|
||||||
|
row: "flex w-full mt-2",
|
||||||
|
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
||||||
|
day: cn(buttonVariants({ variant: "ghost" }), "h-9 w-9 p-0 font-normal aria-selected:opacity-100"),
|
||||||
|
day_range_end: "day-range-end",
|
||||||
|
day_selected:
|
||||||
|
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||||
|
day_today: "bg-accent text-accent-foreground",
|
||||||
|
day_outside:
|
||||||
|
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
|
||||||
|
day_disabled: "text-muted-foreground opacity-50",
|
||||||
|
day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||||
|
day_hidden: "invisible",
|
||||||
|
...classNames,
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
|
||||||
|
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Calendar.displayName = "Calendar";
|
||||||
|
|
||||||
|
export { Calendar };
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Popover = PopoverPrimitive.Root;
|
||||||
|
|
||||||
|
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||||
|
|
||||||
|
const PopoverContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||||
|
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md outline-none",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
));
|
||||||
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent };
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||||
|
import { Circle } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const RadioGroup = React.forwardRef<
|
||||||
|
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return <RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />;
|
||||||
|
});
|
||||||
|
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
const RadioGroupItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"border-primary text-primary ring-offset-background focus-visible:ring-ring aspect-square h-4 w-4 rounded-full border focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||||
|
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
||||||
|
</RadioGroupPrimitive.Indicator>
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
export { RadioGroup, RadioGroupItem };
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -16,10 +16,13 @@
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-avatar": "^1.1.0",
|
"@radix-ui/react-avatar": "^1.1.0",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-select": "^2.2.5",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
|
"@radix-ui/react-radio-group": "^1.3.8",
|
||||||
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
|
|
@ -32,6 +35,7 @@
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
"next": "15.4.4",
|
"next": "15.4.4",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
|
"react-day-picker": "^9.9.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-hook-form": "^7.61.1",
|
"react-hook-form": "^7.61.1",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue