Skip to content

Commit 5b34397

Browse files
committed
Generated add new complex page(WIP)
1 parent a5e9b34 commit 5b34397

11 files changed

Lines changed: 343 additions & 92 deletions

File tree

frontend/src/App.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
22
import ComplexesPage from './pages/ComplexesPage';
33
// import ComplexDetailPage from './pages/ComplexDetailPage'; // 将来的に
4+
import ComplexFormPage from './pages/ComplexFormPage';
45
// import ComplexFormPage from './pages/ComplexFormPage'; // 将来的に
56

67
function App() {
@@ -9,7 +10,7 @@ function App() {
910
<Routes>
1011
<Route path="/" element={<ComplexesPage />} />
1112
{/* 他のルートもここに追加 */}
12-
{/* <Route path="/complexes/new" element={<ComplexFormPage mode="create" />} /> */}
13+
<Route path="/complexes/new" element={<ComplexFormPage />} />
1314
{/* <Route path="/complexes/:id/edit" element={<ComplexFormPage mode="edit" />} /> */}
1415
{/* <Route path="/complexes/:id" element={<ComplexDetailPage />} /> */}
1516
</Routes>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React, { InputHTMLAttributes, forwardRef } from 'react';
2+
import styled from 'styled-components';
3+
4+
const StyledInput = styled.input`
5+
width: 100%;
6+
padding: 0.75rem 1rem; /* 12px 16px */
7+
border: 1px solid #ced4da;
8+
border-radius: 8px;
9+
font-size: 1rem;
10+
color: #495057;
11+
transition:
12+
border-color 0.2s ease-in-out,
13+
box-shadow 0.2s ease-in-out;
14+
15+
&:focus {
16+
border-color: #007aff;
17+
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
18+
outline: none;
19+
}
20+
`;
21+
22+
const Input = forwardRef<
23+
HTMLInputElement,
24+
InputHTMLAttributes<HTMLInputElement>
25+
>((props, ref) => <StyledInput {...props} ref={ref} />);
26+
27+
Input.displayName = 'Input'; // React DevToolsでの表示名
28+
29+
export default Input;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React, { TextareaHTMLAttributes, forwardRef } from 'react';
2+
import styled from 'styled-components';
3+
4+
const StyledTextarea = styled.textarea`
5+
width: 100%;
6+
padding: 0.75rem 1rem; /* 12px 16px */
7+
border: 1px solid #ced4da;
8+
border-radius: 8px;
9+
font-size: 1rem;
10+
color: #495057;
11+
transition:
12+
border-color 0.2s ease-in-out,
13+
box-shadow 0.2s ease-in-out;
14+
min-height: 150px; /* 複数行入力用に高さを確保 */
15+
16+
&:focus {
17+
border-color: #007aff;
18+
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
19+
outline: none;
20+
}
21+
`;
22+
23+
const Textarea = forwardRef<
24+
HTMLTextAreaElement,
25+
TextareaHTMLAttributes<HTMLTextAreaElement>
26+
>((props, ref) => <StyledTextarea {...props} ref={ref} />);
27+
28+
Textarea.displayName = 'Textarea'; // React DevToolsでの表示名
29+
30+
export default Textarea;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React, { ReactNode } from 'react';
2+
import styled from 'styled-components';
3+
4+
const FormGroupWrapper = styled.div`
5+
margin-bottom: 1.5rem; /* 24px */
6+
`;
7+
8+
const Label = styled.label`
9+
display: block;
10+
font-size: 0.9375rem; /* 15px */
11+
font-weight: 500;
12+
color: #343a40;
13+
margin-bottom: 0.5rem; /* 8px */
14+
`;
15+
16+
const ErrorMessage = styled.p`
17+
font-size: 0.875rem; /* 14px */
18+
color: #dc3545; /* Danger color */
19+
margin-top: 0.5rem; /* 8px */
20+
`;
21+
22+
interface FormGroupProps {
23+
label: string;
24+
htmlFor: string;
25+
children: ReactNode;
26+
error?: string;
27+
}
28+
29+
const FormGroup: React.FC<FormGroupProps> = ({
30+
label,
31+
htmlFor,
32+
children,
33+
error,
34+
}) => (
35+
<FormGroupWrapper>
36+
<Label htmlFor={htmlFor}>{label}</Label>
37+
{children}
38+
{error && <ErrorMessage>{error}</ErrorMessage>}
39+
</FormGroupWrapper>
40+
);
41+
42+
export default FormGroup;

frontend/src/components/common/molecules/Header.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import styled from 'styled-components';
33
import { useTranslation } from 'react-i18next';
44
import Button from '../atoms/Button';
5+
import { useNavigate } from 'react-router-dom';
56

67
const HeaderWrapper = styled.header`
78
background-color: rgba(255, 255, 255, 0.8);
@@ -40,6 +41,7 @@ interface HeaderProps {
4041
}
4142

4243
const Header: React.FC<HeaderProps> = ({ onAddNewComplex }) => {
44+
const navigate = useNavigate();
4345
const { t, i18n } = useTranslation();
4446

4547
const changeLanguage = (lng: string) => {
@@ -76,7 +78,7 @@ const Header: React.FC<HeaderProps> = ({ onAddNewComplex }) => {
7678
</Button>
7779
</LanguageSwitcher>
7880
</div>
79-
<div style={{ display: 'flex', alignItems: 'center' }}>
81+
{/* 新規登録ボタンはComplexesPageでのみ表示するため、Headerからは削除 */}
8082
<Button variant="primary" size="small" onClick={onAddNewComplex}>
8183
+ {t('addNewComplexButton')}
8284
</Button>

frontend/src/components/complexes/organisms/NoComplexesMessage.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import styled from 'styled-components';
33
import { useTranslation } from 'react-i18next';
44
import Button from '../../common/atoms/Button';
5+
import { useNavigate } from 'react-router-dom';
56

67
const MessageWrapper = styled.div`
78
margin-top: 3rem; /* 48px */
@@ -42,6 +43,7 @@ interface NoComplexesMessageProps {
4243
const NoComplexesMessage: React.FC<NoComplexesMessageProps> = ({
4344
onAddNewComplex,
4445
}) => {
46+
const navigate = useNavigate();
4547
const { t } = useTranslation();
4648
return (
4749
<MessageWrapper>
@@ -64,7 +66,7 @@ const NoComplexesMessage: React.FC<NoComplexesMessageProps> = ({
6466
<Title>{t('noComplexesTitle')}</Title>
6567
<Subtitle>{t('noComplexesSubtitle')}</Subtitle>
6668
<ButtonWrapper>
67-
<Button variant="primary" onClick={onAddNewComplex}>
69+
<Button variant="primary" onClick={() => navigate('/complexes/new')}>
6870
{t('registerComplexButton')}
6971
</Button>
7072
</ButtonWrapper>

frontend/src/locales/en/translation.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,17 @@
1212
"editButton": "Edit",
1313
"deleteButton": "Delete",
1414
"deleteConfirmation": "Are you sure you want to delete Complex ID {{id}}?",
15-
"lastUpdated": "Last updated: "
15+
"lastUpdated": "Last updated: ",
16+
"complexForm": {
17+
"title": "Register New Complex",
18+
"contentLabel": "Complex Content",
19+
"categoryLabel": "Category",
20+
"contentRequired": "Complex content is required.",
21+
"categoryRequired": "Category is required.",
22+
"submitButton": "Register",
23+
"savingButton": "Saving...",
24+
"cancelButton": "Cancel",
25+
"successMessage": "Complex registered successfully!",
26+
"errorMessage": "Failed to register complex"
27+
}
1628
}

frontend/src/locales/ja/translation.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,17 @@
1212
"editButton": "編集",
1313
"deleteButton": "削除",
1414
"deleteConfirmation": "コンプレックスID {{id}} を本当に削除しますか?",
15-
"lastUpdated": "最終更新: "
15+
"lastUpdated": "最終更新: ",
16+
"complexForm": {
17+
"title": "新しいコンプレックスを登録",
18+
"contentLabel": "コンプレックスの内容",
19+
"categoryLabel": "カテゴリ",
20+
"contentRequired": "コンプレックスの内容は必須です。",
21+
"categoryRequired": "カテゴリは必須です。",
22+
"submitButton": "登録する",
23+
"savingButton": "保存中...",
24+
"cancelButton": "キャンセル",
25+
"successMessage": "コンプレックスを登録しました!",
26+
"errorMessage": "コンプレックスの登録に失敗しました"
27+
}
1628
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import React from 'react';
2+
import styled from 'styled-components';
3+
import { useForm, SubmitHandler } from 'react-hook-form';
4+
import { useMutation, useQueryClient } from '@tanstack/react-query';
5+
import { useTranslation } from 'react-i18next';
6+
import { useNavigate } from 'react-router-dom';
7+
8+
import Header from '../components/common/molecules/Header';
9+
import Button from '../components/common/atoms/Button';
10+
import Input from '../components/common/atoms/Input';
11+
import Textarea from '../components/common/atoms/Textarea';
12+
import FormGroup from '../components/common/molecules/FormGroup';
13+
14+
import type { ComplexInput } from '../types/complex';
15+
import { createComplex } from '../services/api';
16+
17+
const PageWrapper = styled.div`
18+
display: flex;
19+
flex-direction: column;
20+
min-height: 100vh;
21+
`;
22+
23+
const MainContent = styled.main`
24+
flex-grow: 1;
25+
max-width: 800px; /* フォーム用に少し狭く */
26+
margin: 0 auto;
27+
padding: 2rem 1rem; /* 32px 16px */
28+
width: 100%;
29+
`;
30+
31+
const PageTitle = styled.h2`
32+
font-size: 2rem; /* 32px */
33+
font-weight: 700;
34+
color: #1d1d1f;
35+
margin-bottom: 2rem; /* 32px */
36+
text-align: center;
37+
`;
38+
39+
const Form = styled.form`
40+
background-color: #ffffff;
41+
border-radius: 16px;
42+
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.08);
43+
padding: 2rem; /* 32px */
44+
`;
45+
46+
const ActionsWrapper = styled.div`
47+
display: flex;
48+
gap: 1rem; /* 16px */
49+
justify-content: flex-end;
50+
margin-top: 2rem; /* 32px */
51+
`;
52+
53+
const ComplexFormPage: React.FC = () => {
54+
const { t } = useTranslation();
55+
const navigate = useNavigate();
56+
const queryClient = useQueryClient();
57+
58+
const {
59+
register,
60+
handleSubmit,
61+
formState: { errors },
62+
} = useForm<ComplexInput>();
63+
64+
const createComplexMutation = useMutation<Complex, Error, ComplexInput>({
65+
mutationFn: createComplex,
66+
onSuccess: () => {
67+
queryClient.invalidateQueries({ queryKey: ['complexes'] }); // コンプレックス一覧キャッシュを無効化
68+
alert(t('complexForm.successMessage')); // 成功メッセージ
69+
navigate('/'); // 一覧ページへ遷移
70+
},
71+
onError: (error) => {
72+
alert(`${t('complexForm.errorMessage')}: ${error.message}`); // エラーメッセージ
73+
},
74+
});
75+
76+
const onSubmit: SubmitHandler<ComplexInput> = (data) => {
77+
createComplexMutation.mutate(data);
78+
};
79+
80+
const handleCancel = () => {
81+
navigate('/'); // キャンセルで一覧ページへ戻る
82+
};
83+
84+
return (
85+
<PageWrapper>
86+
<Header onAddNewComplex={() => navigate('/complexes/new')} />{' '}
87+
{/* ヘッダーのボタンは自身のページへ */}
88+
<MainContent>
89+
<PageTitle>{t('complexForm.title')}</PageTitle>
90+
<Form onSubmit={handleSubmit(onSubmit)}>
91+
<FormGroup
92+
label={t('complexForm.contentLabel')}
93+
htmlFor="content"
94+
error={errors.content?.message}
95+
>
96+
<Textarea
97+
id="content"
98+
{...register('content', {
99+
required: t('complexForm.contentRequired'),
100+
})}
101+
/>
102+
</FormGroup>
103+
<FormGroup
104+
label={t('complexForm.categoryLabel')}
105+
htmlFor="category"
106+
error={errors.category?.message}
107+
>
108+
<Input
109+
id="category"
110+
type="text"
111+
{...register('category', {
112+
required: t('complexForm.categoryRequired'),
113+
})}
114+
/>
115+
</FormGroup>
116+
<ActionsWrapper>
117+
<Button
118+
variant="secondary"
119+
onClick={handleCancel}
120+
disabled={createComplexMutation.isLoading}
121+
>
122+
{t('complexForm.cancelButton')}
123+
</Button>
124+
<Button
125+
variant="primary"
126+
type="submit"
127+
disabled={createComplexMutation.isLoading}
128+
>
129+
{createComplexMutation.isLoading
130+
? t('complexForm.savingButton')
131+
: t('complexForm.submitButton')}
132+
</Button>
133+
</ActionsWrapper>
134+
</Form>
135+
</MainContent>
136+
{/* <Footer /> */}
137+
</PageWrapper>
138+
);
139+
};
140+
141+
export default ComplexFormPage;

0 commit comments

Comments
 (0)