Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions public/locales/en/editor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"title": "Create Epigram",
"content": "Content",
"content_placeholder": "Please enter within 500 characters.",
"author": "Author",
"enter_directly": "Enter directly",
"unknown": "Unknown",
"self": "Self",
"author_placeholder": "Enter author name",
"source": "Source",
"source_placeholder": "Enter source title",
"tag": "Tag",
"tag_placeholder": "Enter a tag (up to 10 characters)",
"submit": "Completed"
}
15 changes: 15 additions & 0 deletions public/locales/ko/editor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"title": "에피그램 만들기",
"content": "내용",
"content_placeholder": "500자 이내로 입력해주세요.",
"author": "저자",
"enter_directly": "직접 입력",
"unknown": "알 수 없음",
"self": "본인",
"author_placeholder": "저자 이름 입력",
"source": "출처",
"source_placeholder": "출처 제목 입력",
"tag": "태그",
"tag_placeholder": "입력하여 태그 작성 (최대 10자)",
"submit": "작성 완료"
}
21 changes: 21 additions & 0 deletions src/components/Radio/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
type RadioProps = React.InputHTMLAttributes<HTMLInputElement>;

const Radio = (props: RadioProps) => {
return (
<input
type="radio"
{...props}
className="appearance-none h-5 w-5 mr-2
rounded-full border-[2px] border-blue-300 checked:bg-white
relative
before:content-[''] before:absolute
before:inset-1 before:rounded-full
before:bg-blue-800 before:scale-0
checked:before:scale-100
cursor-pointer
transition"
/>
);
};

export default Radio;
35 changes: 35 additions & 0 deletions src/components/RadioGroup/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as React from 'react';
import Radio from '../Radio';

type Option = { label: string; value: string; disabled?: boolean };

type RadioGroupProps = {
name: string;
options: Option[];
value: string;
onChange: (value: string) => void;
className?: string;
};

const RadioGroup = ({ name, options, value, onChange, className = '' }: RadioGroupProps) => {
return (
<fieldset className={className}>
<div className="flex flex-row gap-4 mb-3 lg:mb-4">
{options.map((opt) => (
<label key={opt.value} className="inline-flex items-center cursor-pointer">
<Radio
name={name}
value={opt.value}
checked={value === opt.value}
onChange={(e) => onChange(e.target.value)}
disabled={opt.disabled}
/>
{opt.label}
</label>
))}
</div>
</fieldset>
);
};

export default RadioGroup;
110 changes: 110 additions & 0 deletions src/components/TagInput/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React, { useEffect, useState } from 'react';
import Input from '../Input';
import { useTranslation } from 'next-i18next';

type TagInputProps = {
onChangeTags?: (tags: string[]) => void;
initialTags?: string[];
};

const TagInput = ({ onChangeTags, initialTags }: TagInputProps) => {
const { t } = useTranslation('editor');
const [tags, setTags] = useState<string[]>([]);
const [draft, setDraft] = useState('');
const [isComposing, setIsComposing] = useState(false);

useEffect(() => {
if (initialTags) setTags(initialTags);
}, [initialTags]);

useEffect(() => {
onChangeTags?.(tags);
}, [tags, onChangeTags]);

const add = (raw: string) => {
if (tags.length >= 3) return;
const t = raw.trim();
if (!t) return;
if (t.length > 10) return;
if (tags.includes(t)) return;
setTags((prev) => [...prev, t]);
setDraft('');
};

const removeAt = (i: number) => {
setTags((prev) => prev.filter((_, idx) => idx !== i));
};

const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (isComposing) return;

if (tags.length >= 3) {
e.preventDefault();
return;
}

if (e.key === 'Enter' || e.key === ',') {
e.preventDefault();
add(draft);
return;
}

if (e.key === ',') {
e.preventDefault();
}
};

const onChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;

if (isComposing) {
setDraft(val);
return;
}

const chunks = val.split(/[\n,]+/).map((s) => s.trim());
if (chunks.length === 1) {
setDraft(chunks[0]);
return;
}

const last = chunks.pop() ?? '';
chunks.filter(Boolean).forEach(add);
setDraft(last);
};

const disabled = tags.length >= 3;

return (
<div>
<Input
value={draft}
onChange={onChangeInput}
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={() => setIsComposing(false)}
onKeyDown={onKeyDown}
disabled={disabled}
maxLength={10}
placeholder={t('tag_placeholder')}
/>
{tags.map((t, i) => (
<span
key={`${t}-${i}`}
className="inline-flex items-center gap-1 rounded-full bg-blue-50 px-3 py-1 text-sm text-blue-800"
>
#{t}
<button
type="button"
className="px-1 text-blue-500 hover:text-blue-700"
onClick={() => removeAt(i)}
aria-label={`${t} 태그 삭제`}
>
×
</button>
</span>
))}
</div>
);
};

export default TagInput;
Loading