Skip to content

Commit

Permalink
Merge pull request #29 from /issues/28-feat-edit-page
Browse files Browse the repository at this point in the history
編集画面追加
  • Loading branch information
SatooRu65536 authored Dec 28, 2024
2 parents 7f55df4 + 4b09f3a commit f17551e
Show file tree
Hide file tree
Showing 16 changed files with 596 additions and 160 deletions.
2 changes: 1 addition & 1 deletion app/components/Card/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const CardBody = styled('div', {

export default function Card({ member }: Props) {
const title = useMemo(() => {
if (member.type === 'active' && member.position) {
if (member.type === 'active' && member.position !== null) {
return `${member.lastName} ${member.firstName} (${member.position})`;
}
return `${member.lastName} ${member.firstName}`;
Expand Down
50 changes: 22 additions & 28 deletions app/components/MemberProperty/index.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,36 @@
import { Input, Select, Text } from '@/components/basic';
import dayjs from 'dayjs';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useMemo } from 'react';
import { styled } from 'restyle';

interface StringProperty {
type: 'text';
value: string;
value: string | undefined;
}

interface NumberProperty {
type: 'number';
value: number;
value: number | undefined;
}

interface DateProperty {
type: 'date';
value: Date;
value: Date | undefined;
format?: string;
}

interface IconProperty {
type: 'icon';
value: string;
value: string | undefined;
}

interface SelectProperty {
type: 'select';
value: string;
value: string | undefined;
options: ReadonlyArray<{ key: string; name: string }>;
}

type Props = {
editable?: boolean;
disabled?: boolean;
property: string;
onChange?: (value: string) => void;
} & (StringProperty | NumberProperty | DateProperty | IconProperty | SelectProperty);
export type Property = StringProperty | NumberProperty | DateProperty | IconProperty | SelectProperty;

const SubGrid = styled('div', {
display: 'grid',
Expand All @@ -55,36 +50,35 @@ const IconImage = styled('img', {
height: '100px',
});

export default function MemberProperty({ editable = true, disabled = false, property, value, onChange, ...rest }: Props) {
const [v, setV] = useState<typeof value>(value);
type Props = {
editable?: boolean;
disabled?: boolean;
property: string;
onChange?: (value: string) => void;
} & Property;

export default function MemberProperty({ editable = true, disabled = false, property, value, onChange, ...rest }: Props) {
const valueString = useMemo(() => {
if (rest.type === 'date') {
return dayjs(value).format(rest.format ?? 'YYYY年M月D日');
return dayjs(value).format(editable ? 'YYYY-MM-DD' : rest.format ?? 'YYYY年M月D日');
} else if (rest.type === 'select') {
return rest.options.find((option) => option.key === value)?.name ?? '';
}

return value.toString();
}, [rest, value]);
return value?.toString() ?? '';
}, [editable, rest, value]);

const showValueString = !editable;
const showInput = editable && rest.type !== 'select';
const showSelect = editable && rest.type === 'select';
const showIcon = rest.type === 'icon';

const inputHandle = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setV(e.target.value);
if (onChange) {
onChange(e.target.value);
}
if (onChange) onChange(e.target.value);
}, [onChange]);

const selectHandle = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
setV(e.target.value);
if (onChange) {
onChange(e.target.value);
}
if (onChange) onChange(e.target.value);
}, [onChange]);

return (
Expand All @@ -93,9 +87,9 @@ export default function MemberProperty({ editable = true, disabled = false, prop

<ValueBox>
{showValueString && <Text height="32.5px" nowrap overflow="scroll" size="lg">{valueString}</Text>}
{showInput && <Input disabled={disabled} onChange={inputHandle} value={v.toString()} />}
{showSelect && <Select disabled={disabled} onChange={selectHandle} options={rest.options} value={v.toString()} />}
{showIcon && <IconImage alt="icon" src={v.toString()} />}
{showInput && <Input disabled={disabled} onChange={inputHandle} type={rest.type} value={valueString} />}
{showSelect && <Select disabled={disabled} onChange={selectHandle} options={rest.options} value={valueString} />}
{showIcon && <IconImage alt="icon" src={valueString} />}
</ValueBox>
</SubGrid>
);
Expand Down
47 changes: 30 additions & 17 deletions app/components/PrivateMemberProperties/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import type { Property } from '@/components/MemberProperty';
import type { SetPrivateProperty } from '@/hooks/useEditMember';
import type { Member } from '@/types/member';
import { Text } from '@/components/basic';
import MemberProperty from '@/components/MemberProperty';
import { GENDERS } from '@/consts/member';
import { useCallback } from 'react';
import { styled } from 'restyle';

const SectionTitle = styled('h1', {
textAlign: 'center',
const SectionTitleGroup = styled('div', {
gridColumn: '1 / -1',
});

const SectionDescription = styled('p', {
textAlign: 'center',
gridColumn: '1 / -1',
});

const GridSectopm = styled('section', {
Expand All @@ -24,23 +23,37 @@ const GridSectopm = styled('section', {

interface Props {
editable: boolean;
disabledProperty?: string[];
privateInfo: Member['private'];
setProperty?: SetPrivateProperty;
}

export default function PrivateMemberProperties({ privateInfo, editable }: Props) {
export default function PrivateMemberProperties({ privateInfo, editable, disabledProperty, setProperty }: Props) {
const parseValue = useCallback((value: string, type: Property['type']) => {
if (type === 'number') return Number.parseInt(value);
if (type === 'date') return new Date(value);
return value;
}, []);

const set: SetPrivateProperty = useCallback((key, value) => {
if (setProperty) setProperty(key, value);
}, [setProperty]);

return (
<GridSectopm>
<SectionTitle>非公開情報</SectionTitle>
<SectionDescription>この情報は役員と本人のみ閲覧できます</SectionDescription>
<SectionTitleGroup>
<h1>非公開情報</h1>
<Text>この情報は本人と役員のみ閲覧できます</Text>
</SectionTitleGroup>

<MemberProperty editable={editable} property="メールアドレス" type="text" value={privateInfo.email} />
<MemberProperty editable={editable} property="電話番号" type="text" value={privateInfo.phoneNumber} />
<MemberProperty editable={editable} property="誕生日" type="date" value={privateInfo.birthday} />
<MemberProperty editable={editable} options={GENDERS} property="性別" type="select" value={privateInfo.gender} />
<MemberProperty editable={editable} property="現郵便番号" type="text" value={privateInfo.currentAddress.zipCode} />
<MemberProperty editable={editable} property="現住所" type="text" value={privateInfo.currentAddress.address} />
<MemberProperty editable={editable} property="実家郵便番号" type="text" value={privateInfo.parentAddress.zipCode} />
<MemberProperty editable={editable} property="実家住所" type="text" value={privateInfo.parentAddress.address} />
<MemberProperty disabled={disabledProperty?.includes('email')} editable={editable} onChange={(v) => { set('email', parseValue(v, 'text')); }} property="メールアドレス" type="text" value={privateInfo.email} />
<MemberProperty disabled={disabledProperty?.includes('phoneNumber')} editable={editable} onChange={(v) => { set('phoneNumber', parseValue(v, 'text')); }} property="電話番号" type="text" value={privateInfo.phoneNumber} />
<MemberProperty disabled={disabledProperty?.includes('birthday')} editable={editable} onChange={(v) => { set('birthday', parseValue(v, 'date')); }} property="誕生日" type="date" value={new Date(privateInfo.birthday)} />
<MemberProperty disabled={disabledProperty?.includes('gender')} editable={editable} onChange={(v) => { set('gender', parseValue(v, 'select')); }} options={GENDERS} property="性別" type="select" value={privateInfo.gender} />
<MemberProperty disabled={disabledProperty?.includes('currentAddressZipCode')} editable={editable} onChange={(v) => { set('currentAddressZipCode', parseValue(v, 'text')); }} property="現郵便番号" type="text" value={privateInfo.currentAddressZipCode} />
<MemberProperty disabled={disabledProperty?.includes('currentAddress')} editable={editable} onChange={(v) => { set('currentAddress', parseValue(v, 'text')); }} property="現住所" type="text" value={privateInfo.currentAddress} />
<MemberProperty disabled={disabledProperty?.includes('parentAddressZipCode')} editable={editable} onChange={(v) => { set('parentAddressZipCode', parseValue(v, 'text')); }} property="実家郵便番号" type="text" value={privateInfo.parentAddressZipCode} />
<MemberProperty disabled={disabledProperty?.includes('parentAddress')} editable={editable} onChange={(v) => { set('parentAddress', parseValue(v, 'text')); }} property="実家住所" type="text" value={privateInfo.parentAddress} />
</GridSectopm>
);
}
69 changes: 51 additions & 18 deletions app/components/PublicMemberProperties/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import type { Property } from '@/components/MemberProperty';
import type { SetPublicProperty } from '@/hooks/useEditMember';
import type { Member } from '@/types/member';
import { Text } from '@/components/basic';
import MemberProperty from '@/components/MemberProperty';
import { TYPES } from '@/consts/member';
import { POSITIONS, TYPES } from '@/consts/member';
import { useCallback } from 'react';
import { styled } from 'restyle';

const SectionTitle = styled('h1', {
textAlign: 'center',
const SectionTitleGroup = styled('div', {
gridColumn: '1 / -1',
textAlign: 'center',
});

const GridSectopm = styled('section', {
Expand All @@ -19,31 +23,60 @@ const GridSectopm = styled('section', {

interface Props {
editable: boolean;
publicInfo: Member['public'];
disabledProperty?: string[];
publicInfo: Partial<Member['public']>;
setProperty?: SetPublicProperty;
}

export default function PublicMemberProperties({ publicInfo, editable }: Props) {
const fullName = `${publicInfo.lastName} ${publicInfo.firstName}`;
const fullNameKana = `${publicInfo.lastNameKana} ${publicInfo.firstNameKana}`;
const slackDisplayName = `@${publicInfo.slackDisplayName}`;
export default function PublicMemberProperties({ publicInfo, editable, disabledProperty, setProperty }: Props) {
const parseValue = useCallback((value: string, type: Property['type']) => {
if (type === 'number') return Number.parseInt(value);
if (type === 'date') return new Date(value);
return value;
}, []);

const set: SetPublicProperty = useCallback((key, value) => {
if (setProperty) setProperty(key, value);
}, [setProperty]);

return (
<GridSectopm>
<SectionTitle>公開情報</SectionTitle>
<SectionTitleGroup>
<h1>公開情報</h1>
<Text>この情報は全部員が閲覧できます</Text>
</SectionTitleGroup>

<MemberProperty editable={editable} property="ユーザID" type="text" value={publicInfo.uuid} />
<MemberProperty editable={editable} property="名前" type="text" value={fullName} />
<MemberProperty editable={editable} property="名前(カナ)" type="text" value={fullNameKana} />
<MemberProperty editable={editable} property="Slack表示名" type="text" value={slackDisplayName} />
<MemberProperty editable={editable} property="アイコン" type="icon" value={publicInfo.iconUrl} />
<MemberProperty editable={editable} options={TYPES} property="タイプ" type="select" value={publicInfo.type} />
<MemberProperty disabled={disabledProperty?.includes('uuid')} editable={editable} onChange={(v) => { set('uuid', parseValue(v, 'text')); }} property="ユーザID" type="text" value={publicInfo.uuid} />
<MemberProperty disabled={disabledProperty?.includes('lastName')} editable={editable} onChange={(v) => { set('lastName', parseValue(v, 'text')); }} property="姓" type="text" value={publicInfo.lastName} />
<MemberProperty disabled={disabledProperty?.includes('firstName')} editable={editable} onChange={(v) => { set('firstName', parseValue(v, 'text')); }} property="名" type="text" value={publicInfo.firstName} />
<MemberProperty disabled={disabledProperty?.includes('lastName')} editable={editable} onChange={(v) => { set('lastNameKana', parseValue(v, 'text')); }} property="姓(カナ)" type="text" value={publicInfo.lastNameKana} />
<MemberProperty disabled={disabledProperty?.includes('firstName')} editable={editable} onChange={(v) => { set('firstNameKana', parseValue(v, 'text')); }} property="名(カナ)" type="text" value={publicInfo.firstNameKana} />
<MemberProperty disabled={disabledProperty?.includes('slackDisplayName')} editable={editable} onChange={(v) => { set('slackDisplayName', parseValue(v, 'text')); }} property="Slack表示名" type="text" value={publicInfo.slackDisplayName} />
<MemberProperty disabled={disabledProperty?.includes('iconUrl')} editable={editable} onChange={(v) => { set('iconUrl', parseValue(v, 'icon')); }} property="アイコン" type="icon" value={publicInfo.iconUrl} />
<MemberProperty disabled={disabledProperty?.includes('type')} editable={editable} onChange={(v) => { set('type', TYPES.find((t) => t.name === v)?.key); }} options={TYPES} property="タイプ" type="select" value={publicInfo.type} />

{
publicInfo.type === 'active' && (
<>
<MemberProperty editable={editable} property="学籍番号" type="text" value={publicInfo.studentId} />
<MemberProperty editable={editable} property="卒業予定年度" type="number" value={publicInfo.expectedGraduationYear} />
<MemberProperty editable={editable} property="役職" type="text" value={publicInfo.position ?? '-'} />
<MemberProperty disabled={disabledProperty?.includes('studentId')} editable={editable} onChange={(v) => { set('studentId', parseValue(v, 'text')); }} property="学籍番号" type="text" value={publicInfo.studentId} />
<MemberProperty disabled={disabledProperty?.includes('expectedGraduationYear')} editable={editable} onChange={(v) => { set('expectedGraduationYear', parseValue(v, 'number')); }} property="卒業予定年度" type="number" value={publicInfo.expectedGraduationYear ?? Number.NaN} />
<MemberProperty disabled={disabledProperty?.includes('position')} editable={editable} onChange={(v) => { set('position', POSITIONS.find((p) => p === v)); }} options={['-', ...POSITIONS].map((p) => ({ key: p, name: p }))} property="役職" type="select" value={publicInfo.position ?? '-'} />
</>
)
}
{
publicInfo.type === 'alumni' && (
<>
<MemberProperty disabled={disabledProperty?.includes('graduationYear')} editable={editable} onChange={(v) => { set('graduationYear', parseValue(v, 'number')); }} property="卒業年度" type="number" value={publicInfo.graduationYear ?? Number.NaN} />
<MemberProperty disabled={disabledProperty?.includes('oldPosition')} editable={editable} onChange={(v) => { set('oldPosition', parseValue(v, 'text')); }} property="旧役職" type="text" value={publicInfo.oldPosition ?? undefined} />
</>
)
}
{
publicInfo.type === 'external' && (
<>
<MemberProperty disabled={disabledProperty?.includes('schoolName')} editable={editable} onChange={(v) => { set('schoolName', parseValue(v, 'text')); }} property="学校名" type="text" value={publicInfo.schoolName} />
<MemberProperty disabled={disabledProperty?.includes('organization')} editable={editable} onChange={(v) => { set('organization', parseValue(v, 'text')); }} property="団体名" type="text" value={publicInfo.organization} />
</>
)
}
Expand Down
8 changes: 5 additions & 3 deletions app/components/basic/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@ import { css, styled } from 'restyle';

const ButtonBase = styled('button', {
padding: '10px 12px',
color: 'var(--on-primary-color)',
backgroundColor: 'var(--primary-color)',
border: 'none',
borderRadius: 'var(--radius-sm)',
cursor: 'pointer',
});

interface Props extends React.ComponentProps<typeof ButtonBase> {
size?: 'sm' | 'md' | 'lg' | 'xl';
outline?: boolean;
}

export default function Button({ children, size = 'md', ...props }: Props) {
export default function Button({ children, size = 'md', outline = false, ...props }: Props) {
const [className, Styles] = css({
fontSize: `var(--fontsize-${size})`,
color: outline ? 'var(--primary-color)' : 'var(--on-primary-color)',
backgroundColor: outline ? 'var(--on-primary-color)' : 'var(--primary-color)',
outline: outline ? '1px solid var(--primary-color)' : 'none',
});

return (
Expand Down
80 changes: 80 additions & 0 deletions app/hooks/useEditMember.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { TYPES } from '@/consts/member';
import type { ActiveMember, AlumniMember, ExternalMember, Member, MemberBasePrivateInfo, MemberPublicInfo } from '@/types/member';
import { useCallback, useMemo, useState } from 'react';

export type SetPublicProperty = (key: keyof EditaingMember['public'], value: unknown) => void;
export type SetPrivateProperty = (key: keyof EditaingMember['private'], value: unknown) => void;

export interface EditaingMember {
public: Omit<
Partial<ActiveMember>
& Partial<AlumniMember>
& Partial<ExternalMember>,
'type'
> & { type: typeof TYPES[number]['key'] };
private: Partial<MemberBasePrivateInfo>;
}

export default function useEditMember(init: Member | MemberPublicInfo) {
const [editing, setEditing] = useState<EditaingMember>({ private: {}, ...init });

const setPublicProperty: SetPublicProperty = useCallback((key: keyof EditaingMember['public'], value: unknown) => {
setEditing((prev) => ({ ...prev, public: { ...prev.public, [key]: value } }));
}, []);

const setPrivateProperty: SetPrivateProperty = useCallback((key: keyof EditaingMember['private'], value: unknown) => {
setEditing((prev) => ({ ...prev, private: { ...prev.private, [key]: value } }));
}, []);

const member = useMemo(() => toMember(editing), [editing]);
return { member, setPublicProperty, setPrivateProperty } as const;
}

function toMember(editing: EditaingMember) {
const { public: publicInfo, private: privateInfo } = editing;
const baseInfo = {
uuid: publicInfo.uuid,
iconUrl: publicInfo.iconUrl,
firstName: publicInfo.firstName,
lastName: publicInfo.lastName,
firstNameKana: publicInfo.firstNameKana,
lastNameKana: publicInfo.lastNameKana,
slackDisplayName: publicInfo.slackDisplayName,
type: publicInfo.type,
};

switch (publicInfo.type) {
case 'active':
return {
private: privateInfo,
public: {
...baseInfo,
studentId: publicInfo.studentId,
grade: publicInfo.grade,
expectedGraduationYear: publicInfo.expectedGraduationYear,
position: publicInfo.position,
},
};

case 'alumni':
return {
private: privateInfo,
public: {
...baseInfo,
oldPosition: publicInfo.oldPosition,
graduationYear: publicInfo.graduationYear,
},
};

case 'external':
return {
private: privateInfo,
public: {
...baseInfo,
schoolName: publicInfo.schoolName,
organization: publicInfo.organization,
expectedGraduationYear: publicInfo.expectedGraduationYear,
},
};
}
}
Loading

0 comments on commit f17551e

Please sign in to comment.