import { CurrentScreenContext } from "../../hooks/useCurrentScreen";
import { ProjectContext } from "../../hooks/useProject";
import { capitalizeFirst } from "../../lib/stringUtils";
import { ComponentType } from "../../types/component"
import type { ButtonComponent, Component, TextComponent, TextFieldComponent } from "../../types/component";
import type { Screen } from "../../types/screen"
import { useContext, useState } from "react";
import { FiEdit3, FiMoreHorizontal, FiTrash2, FiX } from "react-icons/fi"
export function ScreenList() {
const projectContext = useContext(ProjectContext);
const curScreenContext = useContext(CurrentScreenContext);
const [showAddModal, setShowAddModel] = useState(false);
const [editIndex, setEditIndex] = useState(-1);
const [editName, setEditName] = useState("");
const [activeDropdown, setActiveDropdown] = useState(-1);
const [showTemplateModal, setShowTemplateModal] = useState(false);
const handleShowAddModal = () => {
setEditIndex(-1)
setEditName("");
setShowAddModel(true);
}
const closeDropdown = () => {
setActiveDropdown(-1);
}
const startEdit = (index: number, curName: string) => {
setEditName(curName);
setEditIndex(index);
setShowAddModel(true);
}
if (projectContext === null || curScreenContext === null) return <div>loading...</div>
const screens = projectContext.projectStates.screens;
const projectActions = projectContext.projectActions;
const curScreenState = curScreenContext.curScreenStates;
const curScreenActions = curScreenContext.curScreenActions;
return (
<div className="p-4 bg-white w-full">
<div className="flex items-center justify-between mb-4 w-full">
<h3 className="text-lg font-semibold text-gray-900">スクリーン</h3>
</div>
<div className="space-y-2">
{screens.map((screen, index) => (
<ScreenCard
key={index}
screen={screen}
index={index}
selected={index === curScreenState.curScreenIndex}
setCurScreenIndex={curScreenActions.changeCurScreen}
activeDropdown={activeDropdown}
setActiveDropdown={setActiveDropdown}
isLast={screens.length === 1}
closeDropdown={closeDropdown}
deleteScreen={projectActions.deleteScreen}
startEdit={startEdit}
selectComponent={curScreenActions.selectComponent}
/>
))}
</div>
<div className="mt-4 pt-4 border-t border-gray-100">
<button
onClick={handleShowAddModal}
className="w-full flex items-center justify-center p-3 text-sm font-medium text-blue-600 bg-blue-50 hover:bg-blue-100 rounded-xl transition-all duration-200 border border-blue-200 hover:border-blue-300"
>
<span className="mr-2 text-lg">+</span>
新しいスクリーンを追加
</button>
<button
onClick={() => setShowTemplateModal(true)}
className="mt-4 w-full flex items-center justify-center p-3 text-sm font-medium text-blue-600 bg-blue-50 hover:bg-blue-100 rounded-xl transition-all duration-200 border border-blue-200 hover:border-blue-300"
>
<span className="mr-2 text-lg">+</span>
テンプレートから追加
</button>
</div>
{/* Click outside to close dropdown */}
{activeDropdown !== -1 && (
<div
className="fixed inset-0 z-5"
onClick={() => setActiveDropdown(-1)}
/>
)}
{showAddModal && <AddScreenModal
close={() => setShowAddModel(false)}
screen={screens[editIndex]}
addScreen={projectActions.addScreen}
editIndex={editIndex}
editName={editName}
selectComponent={curScreenActions.selectComponent}
updateScreen={projectActions.updateScreen}
/>}
{showTemplateModal && <AddTemplateModal
close={() => setShowTemplateModal(false)}
/>}
</div>
)
}
interface ScreenCardProps {
screen: Screen,
selected: boolean,
setCurScreenIndex: (index: number) => void
index: number,
activeDropdown: number,
setActiveDropdown: (index: number) => void,
isLast: boolean,
closeDropdown: () => void,
deleteScreen: (screenId: string) => void,
startEdit: (index: number, curName: string) => void
selectComponent: (index: number) => void
}
function ScreenCard({
screen,
selected,
setCurScreenIndex,
index,
activeDropdown,
setActiveDropdown,
isLast,
closeDropdown,
deleteScreen,
startEdit,
selectComponent
}: ScreenCardProps) {
const handleSelectScreen = (index: number) => {
selectComponent(-1);
setCurScreenIndex(index)
}
const handleEditAction = (index: number, name: string) => {
startEdit(index, name)
closeDropdown()
};
const handleDeleteAction = () => {
deleteScreen(screen.id);
closeDropdown()
};
return (
<div
className={`group relative rounded-xl border transition-all duration-200 hover:border-gray-300 hover:shadow-sm bg-sky-100 border-gray-200 ${selected ? "bg-sky-100 border-gray-400" : "bg-white border-gray-200"}`}
>
<div onClick={() => { handleSelectScreen(index) }} className="flex items-center p-3 cursor-pointer">
<div className="flex-1 min-w-0">
<div className="font-medium text-sm truncate text-gray-900">
{screen.name}
</div>
</div>
<button
onClick={() => { setActiveDropdown(index) }}
className="p-1.5 rounded-lg transition-all duration-200 text-gray-400 hover:text-gray-600 hover:bg-gray-100 opacity-0 group-hover:opacity-100"
>
<FiMoreHorizontal size={14} />
</button>
</div>
{activeDropdown === index && (
<div className="absolute right-2 top-12 z-10 bg-white rounded-lg shadow-lg border border-gray-200 py-1 min-w-[140px]">
<button
onClick={() => handleEditAction(index, screen.name)}
className="flex items-center w-full px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
>
<FiEdit3 size={14} className="mr-2" />
編集
</button>
{!isLast && (
<>
<hr className="my-1 border-gray-100" />
<button
onClick={() => handleDeleteAction()}
className="flex items-center w-full px-3 py-2 text-sm text-red-600 hover:bg-red-50 transition-colors"
>
<FiTrash2 size={14} className="mr-2" />
削除
</button>
</>
)}
</div>
)}
</div>
)
}
interface AddScreenModalProps {
close: () => void,
screen: Screen
addScreen: (name: string) => void,
editIndex: number,
editName: string,
updateScreen: (screen: Screen) => void
selectComponent: (index: number) => void
}
function AddScreenModal({ close, screen, addScreen, editName, editIndex, updateScreen, selectComponent }: AddScreenModalProps) {
const [newScreenName, setNewScreenName] = useState<string>(editIndex !== -1 ? editName : "");
const handleCancelCreate = () => {
setNewScreenName("");
close();
}
const handleCreateConfirm = () => {
selectComponent(-1);
if (editIndex === -1) {
addScreen(newScreenName);
} else {
updateScreen({ ...screen, name: newScreenName });
}
setNewScreenName("");
close();
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white rounded-xl shadow-xl p-6 w-96 max-w-[90vw]">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">新しいスクリーンを作成</h3>
<button
onClick={handleCancelCreate}
className="p-1 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100 transition-colors"
>
<FiX size={20} />
</button>
</div>
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
スクリーン名
</label>
<input
type="text"
value={newScreenName}
onChange={(e) => setNewScreenName(e.target.value)}
placeholder="スクリーン名を入力してください"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
autoFocus
/>
</div>
<div className="flex gap-3 justify-end">
<button
onClick={handleCancelCreate}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
>
キャンセル
</button>
<button
onClick={handleCreateConfirm}
disabled={!newScreenName.trim()}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed rounded-lg transition-colors"
>
作成
</button>
</div>
</div>
</div>
)
}
interface AddTemplateModalProps {
close: () => void,
}
function AddTemplateModal({ close }: AddTemplateModalProps) {
const [type, setType] = useState<"add" | "list" | "delete" | "update" | "search" | "">("");
const [screenName, setScreenName] = useState("");
const handleOutsideClick = () => {
close();
}
const handleTypeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedType = e.target.value as "add" | "list" | "delete" | "update" | "search" | "";
setType(selectedType);
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50" onClick={handleOutsideClick}>
<div className="w-full bg-white rounded-xl shadow-xl p-6 w-96 max-w-[70vw]" onClick={(e) => { e.stopPropagation() }}>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">テンプレートを選択</h3>
<button
onClick={() => close()}
className="p-1 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100 transition-colors"
>
<FiX size={20} />
</button>
</div>
<div className="flex w-full mb-4">
<label className="mr-4 mt-2 flex-1">画面名</label>
<input
type="text"
className="border rounded flex-10 px-3"
onChange={(e) => setScreenName(e.target.value)}
value={screenName}
/>
</div>
<div className="flex w-full mb-4">
<label className="mr-4 mt-2 flex-1">
タイプを選択
</label>
<select
className="border px-3 py-2 rounded flex-10"
onChange={handleTypeChange}
>
<option value={""}>
選択してください...
</option>
<option value={"add"}>
追加画面
</option>
<option value={"list"}>
一覧画面
</option>
<option value={"update"}>
編集画面
</option>
<option value={"delete"}>
削除画面
</option>
<option value={"search"}>
検索画面
</option>
</select>
</div>
{type === "add" && <AddTemplate
screenName={screenName}
close={close}
/>}
</div>
</div>
)
}
interface AddTemplateProps {
screenName: string
close: () => void
}
function AddTemplate({ screenName, close }: AddTemplateProps) {
const projectContext = useContext(ProjectContext);
const curScreenContext = useContext(CurrentScreenContext);
const [selectedTableId, setSelectedTableId] = useState("");
const [successScreen, setSuccessScreen] = useState("");
if (projectContext === null || curScreenContext === null) return <div>loading...</div>
const tables = projectContext.projectStates.tables;
const projectActions = projectContext.projectActions;
const projectStates = projectContext.projectStates;
const curScreenActions = curScreenContext.curScreenActions;
const confirm = async () => {
const MARGIN_X = 50;
const COLUMN_MARGIN_Y = 20;
const LABEL_MARGIN_Y = 25;
const TEXT_WIDTH = 650;
const TEXT_HEIGHT = 25;
const INPUT_WIDTH = 650;
const INPUT_HEIGHT = 40;
const screenId = projectActions.addScreen(screenName);
curScreenActions.selectComponent(-1);
const table = tables.find((table) => table.id === selectedTableId);
const actionData: { [key: string]: string } = {};
const components: Component[] = [];
for (let i = 0; i < (table?.columns ?? []).length; i++) {
const column = table?.columns[i];
let columnName;
if (!(column?.foreignTableName)) {
columnName = column?.name;
} else {
const foreignTable = tables.find((table) => table.name === column.foreignTableName);
columnName = `${foreignTable?.columns.find(col => col.isPrimary)?.name}Of${capitalizeFirst(foreignTable?.name)}`;
}
const label: TextComponent = {
id: "w" + self.crypto.randomUUID().replace(/-/g, ""),
screenId: screenId,
index: i * 2,
name: `${table?.name}${capitalizeFirst(columnName)}Label`,
type: ComponentType.Text,
uiData: {
posX: MARGIN_X * (i % 2 + 1) + INPUT_WIDTH * (i % 2),
posY: COLUMN_MARGIN_Y * (Math.floor(i / 2) + 1) + (TEXT_HEIGHT + INPUT_HEIGHT + LABEL_MARGIN_Y) * (Math.floor(i / 2)),
width: TEXT_WIDTH,
height: TEXT_HEIGHT
},
generalData: {
text: `${columnName}`
},
style: {}
}
const field: TextFieldComponent = {
id: "w" + self.crypto.randomUUID().replace(/-/g, ""),
screenId: screenId,
index: i * 2 + 1,
name: `${table?.name}${capitalizeFirst(columnName)}Input`,
type: ComponentType.TextField,
uiData: {
posX: MARGIN_X * (i % 2 + 1) + INPUT_WIDTH * (i % 2),
posY: (COLUMN_MARGIN_Y + LABEL_MARGIN_Y) * (Math.floor(i / 2) + 1) + (TEXT_HEIGHT + INPUT_HEIGHT) * (Math.floor(i / 2)),
width: INPUT_WIDTH,
height: INPUT_HEIGHT
},
generalData: {
text: ""
},
style: {}
}
actionData[columnName ?? ""] = `\${${table?.name}${capitalizeFirst(columnName)}Input}`
components.push(label);
components.push(field);
}
const confirmButton: ButtonComponent = {
id: "w" + self.crypto.randomUUID().replace(/-/g, ""),
name: `add${capitalizeFirst(table?.name)}ConfirmButton`,
screenId: screenId,
index: (table?.columns.length ?? 0) * 2,
type: ComponentType.Button,
uiData: {
posX: 1200,
posY: 750,
width: 150,
height: 40
},
generalData: {
text: "登録"
},
style: {},
action: {
type: "addData",
action: {
tableName: table?.name ?? "",
newData: actionData,
successScreenName: successScreen,
failScreenName: ""
}
}
}
const backButton: ButtonComponent = {
id: "w" + self.crypto.randomUUID().replace(/-/g, ""),
name: `add${capitalizeFirst(table?.name)}BackButton`,
screenId: screenId,
index: (table?.columns.length ?? 0) * 2 + 1,
type: ComponentType.Button,
uiData: {
posX: 200,
posY: 750,
width: 150,
height: 40
},
generalData: {
text: "戻る"
},
style: {},
action: {
type: "navigate",
action: {
nextScreenName: "_back"
}
}
}
components.push(confirmButton);
components.push(backButton)
projectActions.addComponents(screenId, components);
close();
}
return (
<div className="flex flex-col gap-4">
<div className="flex">
<label className="mr-4 mt-2 flex-1">対象テーブル</label>
<select
className="border px-3 py-2 rounded flex-10"
value={selectedTableId}
onChange={(e) => { setSelectedTableId(e.target.value) }}
>
<option value={""}>選択してください...</option>
{tables.map((table, idx) => (
<option key={idx} value={table.id}>
{table.name}
</option>
))}
</select>
</div>
<div className="flex">
<label className="mr-4 mt-2 flex-1">成功時遷移先</label>
<select
className="border px-3 py-2 rounded flex-10"
value={successScreen}
onChange={(e) => { setSuccessScreen(e.target.value) }}
>
<option value={""}>選択してください...</option>
<option value="_back">戻る</option>
{projectStates.screens.map((screen, idx) => (
<option key={idx} value={screen.name}>
{screen.name}
</option>
))}
</select>
</div>
<div className="flex justify-end">
<button className="px-4 py-2 bg-blue-600 text-white rounded-lg
hover:bg-blue-700 active:scale-95
transition duration-200 disabled:bg-gray-400 disabled:cursor-not-allowed disabled:opacity-70"
onClick={confirm}
>
追加
</button>
</div>
</div>
)
}