Skip to content

Commit

Permalink
Merge pull request #36 from daodaoedu/feature/group-create
Browse files Browse the repository at this point in the history
✨ create group page and form
  • Loading branch information
JohnsonMao authored Feb 13, 2024
2 parents 4c88a0d + 939f305 commit c62a1be
Show file tree
Hide file tree
Showing 18 changed files with 675 additions and 153 deletions.
7 changes: 1 addition & 6 deletions components/Group/Banner.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,6 @@ const StyledBanner = styled.div`
const Banner = () => {
const router = useRouter();

const handleClick = () => {
// TODO: 判斷是否登入決定按鈕導向哪個頁面
router.push('/login');
};

return (
<StyledBanner>
<picture>
Expand All @@ -65,7 +60,7 @@ const Banner = () => {
<h1>揪團</h1>
<p>想一起組織有趣的活動或學習小組嗎?</p>
<p>註冊並加入我們,然後創建你的活動,讓更多人一起參加!</p>
<Button onClick={handleClick}>我想揪團</Button>
<Button onClick={() => router.push('/group/create')}>我想揪團</Button>
</div>
</StyledBanner>
);
Expand Down
36 changes: 36 additions & 0 deletions components/Group/Form/Fields/AreaCheckbox.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Box from '@mui/material/Box';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import Select from './Select';

export default function AreaCheckbox({
options,
itemLabel,
itemValue,
name,
value,
onChange,
}) {
return (
<>
<Box sx={{ display: 'flex', label: { whiteSpace: 'nowrap' } }}>
<FormControlLabel control={<Checkbox />} label="實體活動" />
<Select
name={name}
options={options}
placeholder="地點"
value={value}
itemLabel={itemLabel}
itemValue={itemValue}
onChange={onChange}
/>
</Box>
<div>
<FormControlLabel control={<Checkbox />} label="線上" />
</div>
<div>
<FormControlLabel control={<Checkbox />} label="待討論" />
</div>
</>
);
}
58 changes: 58 additions & 0 deletions components/Group/Form/Fields/Select.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import FormControl from '@mui/material/FormControl';
import MuiSelect from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';

export default function Select({
id,
name,
value,
placeholder,
options = [],
itemLabel = 'label',
fullWidth = true,
multiple,
onChange,
sx,
}) {
const getValue = (any, key) => (typeof any === 'object' ? any[key] : any);
const renderValue = (selected) => {
if (selected.length === 0) return placeholder;
if (Array.isArray(selected)) return selected.join('、');
return selected;
};

return (
<FormControl size="small" fullWidth>
<MuiSelect
displayEmpty
multiple={multiple}
fullWidth={fullWidth}
renderValue={renderValue}
id={id}
name={name}
sx={{
color: value.length ? '#000' : '#92989A',
'& legend': { display: 'none' },
'& fieldset': { top: 0 },
...sx,
}}
value={value}
onChange={onChange}
>
{placeholder && (
<MenuItem disabled value="" sx={{ fontSize: 14 }}>
{placeholder}
</MenuItem>
)}
{options.map((item) => (
<MenuItem
key={getValue(item, itemLabel)}
value={getValue(item, itemLabel)}
>
{getValue(item, itemLabel)}
</MenuItem>
))}
</MuiSelect>
</FormControl>
);
}
65 changes: 65 additions & 0 deletions components/Group/Form/Fields/TagsField.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useState } from 'react';
import IconButton from '@mui/material/IconButton';
import FormHelperText from '@mui/material/FormHelperText';
import ClearIcon from '@mui/icons-material/Clear';
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
import { StyledChip, StyledTagsField } from '../Form.styled';

function TagsField({ label, helperText, ...props }) {
const [tags, setTags] = useState([]);
const [input, setInput] = useState('');
const [error, setError] = useState('');

const handleInput = (e) => {
const { value } = e.target;
if (value.length > 8) setError('標籤最多 8 個字');
else setError('');
setInput(value);
};

const handleKeyDown = (e) => {
if (error) return;
const tag = input.trim();
if (e.key !== 'Enter' || !tag) return;
if (tags.indexOf(tag) > -1) return;
setTags((pre) => [...pre, tag]);
setInput('');
};

const handleDelete = (tag) => () => {
setTags((pre) => pre.filter((t) => t !== tag));
};

return (
<>
<StyledTagsField>
{tags.map((tag) => (
<StyledChip
key={tag}
label={tag}
size="small"
deleteIcon={<ClearIcon />}
onDelete={handleDelete(tag)}
/>
))}
{tags.length < 8 && (
<input
{...props}
value={input}
onChange={handleInput}
onKeyDown={handleKeyDown}
/>
)}
{input.trim() && (
<IconButton sx={{ textTransform: 'none' }} size="small" edge="end">
<AddCircleOutlineIcon />
</IconButton>
)}
</StyledTagsField>
<FormHelperText>{helperText}</FormHelperText>
<span className="error-message">{error}</span>
</>
);
}

export default TagsField;
40 changes: 40 additions & 0 deletions components/Group/Form/Fields/TextField.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import MuiTextField from '@mui/material/TextField';
import { useEffect, useState } from 'react';

export default function TextField({
id,
placeholder,
multiline,
name,
value,
onChange,
helperText,
max,
errorMessage,
}) {
const [error, setError] = useState('');

useEffect(() => {
if (value.length > max) setError(errorMessage);
else setError('');
}, [max, value]);

return (
<>
<MuiTextField
fullWidth
id={id}
name={name}
sx={{ '& legend': { display: 'none' } }}
size="small"
placeholder={placeholder}
value={value}
multiline={multiline}
rows={multiline && 10}
onChange={onChange}
helperText={helperText}
/>
<span className="error-message">{error}</span>
</>
);
}
108 changes: 108 additions & 0 deletions components/Group/Form/Fields/Upload.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { useRef, useState } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import DeleteSvg from '@/public/assets/icons/delete.svg';
import { StyledUpload } from '../Form.styled';
import UploadSvg from './UploadSvg';

export default function Upload({ name, onChange }) {
const [preview, setPreview] = useState('');
const [error, setError] = useState('');
const inputRef = useRef();

const changeHandler = (file) => {
const event = {
target: {
name,
value: file,
},
};
onChange(event);
};

const handleFile = (file) => {
const imageType = /image.*/;
setError('');
if (!file.type.match(imageType)) {
setError('僅支援上傳圖片唷!');
return;
}

const reader = new FileReader();
reader.onload = (e) => setPreview(e.target.result);
reader.readAsDataURL(file);
changeHandler(file);
};

const handleDragEnter = (e) => {
e.stopPropagation();
e.preventDefault();
};

const handleDragOver = (e) => {
e.stopPropagation();
e.preventDefault();
};

const handleDrop = (e) => {
e.stopPropagation();
e.preventDefault();

const { files } = e.dataTransfer;
if (files?.[0]) handleFile(files[0]);
};

const handleChange = (e) => {
const { files } = e.target;
if (files?.[0]) handleFile(files[0]);
};

const handleClear = () => {
setPreview('');
setError('');
inputRef.current.value = '';
changeHandler(null);
};

return (
<Box sx={{ position: 'relative' }}>
{preview && (
<Button
variant="text"
size="small"
sx={{
position: 'absolute',
right: 0,
top: '-2rem',
gap: 0.25,
span: {
marginTop: 0.25,
},
}}
onClick={handleClear}
>
<img src={DeleteSvg.src} alt="delete icon" />
<span>清除</span>
</Button>
)}
<StyledUpload
className={preview ? 'has-image' : ''}
onClick={() => inputRef.current.click()}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDrop={handleDrop}
>
{preview && <img className="preview" src={preview} alt="預覽封面圖" />}
<UploadSvg isActive={!!preview} />
<span>{preview ? '上傳其他圖片' : '點擊此處或將圖片拖曳至此'}</span>
<input
type="file"
ref={inputRef}
accept="image/*"
onChange={handleChange}
/>
</StyledUpload>
<span className="error-message">{error}</span>
</Box>
);
}
18 changes: 18 additions & 0 deletions components/Group/Form/Fields/UploadSvg.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export default function UploadSvg({ isActive }) {
const fillColor = isActive ? '#FFFFFF' : '#89DAD7';

return (
<svg
width="40"
height="33"
viewBox="0 0 40 33"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0 4.5C0 2.3 1.8 0.5 4 0.5H36C37.0609 0.5 38.0783 0.921427 38.8284 1.67157C39.5786 2.42172 40 3.43913 40 4.5V28.5C40 29.5609 39.5786 30.5783 38.8284 31.3284C38.0783 32.0786 37.0609 32.5 36 32.5H4C2.93913 32.5 1.92172 32.0786 1.17157 31.3284C0.421427 30.5783 0 29.5609 0 28.5V4.5ZM22 22.5L16 16.5L4 28.5H36L26 18.5L22 22.5ZM30 14.5C31.0609 14.5 32.0783 14.0786 32.8284 13.3284C33.5786 12.5783 34 11.5609 34 10.5C34 9.43913 33.5786 8.42172 32.8284 7.67157C32.0783 6.92143 31.0609 6.5 30 6.5C28.9391 6.5 27.9217 6.92143 27.1716 7.67157C26.4214 8.42172 26 9.43913 26 10.5C26 11.5609 26.4214 12.5783 27.1716 13.3284C27.9217 14.0786 28.9391 14.5 30 14.5Z"
fill={fillColor}
/>
</svg>
);
}
37 changes: 37 additions & 0 deletions components/Group/Form/Fields/Wrapper.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Tooltip from '@mui/material/Tooltip';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import { StyledLabel, StyledGroup } from '../Form.styled';

const popperProps = {
popper: {
modifiers: [
{
name: 'offset',
options: {
offset: [0, -14],
},
},
],
},
};

export default function Wrapper({ id, required, label, children, tooltip }) {
return (
<StyledGroup>
<StyledLabel htmlFor={id} required={required}>
{label}
{tooltip && (
<Tooltip
title={tooltip}
slotProps={popperProps}
placement="top"
arrow
>
<InfoOutlinedIcon sx={{ width: 14, height: 14, mx: '4px' }} />
</Tooltip>
)}
</StyledLabel>
{children}
</StyledGroup>
);
}
Loading

0 comments on commit c62a1be

Please sign in to comment.