Skip to content

Commit

Permalink
Merge pull request #85 from Next-Room/feat/hint-qa4
Browse files Browse the repository at this point in the history
  • Loading branch information
lgrin-byte authored Nov 30, 2024
2 parents 90bb0ed + 2ab8bdd commit a993812
Show file tree
Hide file tree
Showing 19 changed files with 262 additions and 65 deletions.
2 changes: 2 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
}
],
"react/display-name": "off",
"jsx-a11y/alt-text": "off",
"@next/next/no-img-element": "off",
"no-console": ["error", { "allow": ["warn", "error"] }]
}
}
53 changes: 53 additions & 0 deletions app/(shared)/(ThemeTextArea)/Container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from "react";
import "./textArea.modules.sass";
import classNames from "classnames";

import useTextArea from "./useTextArea";
import { ThemeInfoTextAreaType } from "./TextAreaType";

export default function ThemeTextArea({
id,
tabIndex,
content,
infoText,
textAreaPlaceholder,
checkErrorText,
}: ThemeInfoTextAreaType) {
const {
textAreaValue,
isFocus,
setIsFocus,
errorText,
textAreaRef,
handleTextAreaChange,
handleTextAreaBlur,
} = useTextArea({ id, content, checkErrorText });

return (
<div tabIndex={isFocus ? -1 : tabIndex} onFocus={() => setIsFocus(true)}>
<textarea
ref={textAreaRef}
className={classNames("theme-textarea", {
error: errorText,
filled: textAreaValue && !(errorText || isFocus),
})}
value={textAreaValue}
placeholder={textAreaPlaceholder}
onChange={handleTextAreaChange}
onBlur={handleTextAreaBlur}
tabIndex={tabIndex}
/>

{errorText && (
<div className="theme-textfield-info error" tabIndex={-1}>
{errorText}
</div>
)}
{infoText && (
<div className="theme-textfield-info" tabIndex={-1}>
{infoText}
</div>
)}
</div>
);
}
11 changes: 11 additions & 0 deletions app/(shared)/(ThemeTextArea)/TextAreaType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
type ValidationFunction<T> = (value: T) => string;

export type ThemeInfoTextAreaType = {
id: "contents" | "answer";
tabIndex?: number;
title?: string;
content: string;
infoText?: string;
textAreaPlaceholder?: string;
checkErrorText?: ValidationFunction<unknown>;
};
38 changes: 38 additions & 0 deletions app/(shared)/(ThemeTextArea)/textArea.modules.sass
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
@import '../../style/variables'
@import '../../style/mixins'
@import '../../style/button'

.theme-textarea
width: 100%
min-height: 120px
padding: 8px 12px
display: flex
border: 1px solid $color-white20
border-radius: 8px
background-color: $color-white5
color: $color-white
resize: none
cursor: pointer

&.filled
background-color: $color-main
&:focus
outline: none
border: 1px solid $color-white
background-color: $color-black
cursor: text
&:hover
background-color: $color-black
&.error
border: 1px solid $color-semantic100
cursor: text

.theme-textfield-info
margin: 4px 0 0 16px
cursor: default

@include body12R
color: $color-white70
&.error
color: $color-semantic100

76 changes: 76 additions & 0 deletions app/(shared)/(ThemeTextArea)/useTextArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { ChangeEvent, FocusEvent, useEffect, useRef, useState } from "react";

import { useCreateTheme } from "@/components/atoms/createTheme.atom";
import { useCreateHint } from "@/components/atoms/createHint.atom";

import { ThemeInfoTextAreaType } from "./TextAreaType";

const useTextArea = ({
id,
content,
checkErrorText,
}: ThemeInfoTextAreaType) => {
const [textAreaValue, setTextAreaValue] = useState<string>(content || "");
const [isFocus, setIsFocus] = useState<boolean>(false);
const [errorText, setErrorText] = useState<string>("");
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const [, setCreateHint] = useCreateHint();
const [, setCreateTheme] = useCreateTheme();

useEffect(() => {
if (errorText) return;
setCreateTheme((prev) => ({
...prev,
[id]: textAreaValue,
}));
setCreateHint((prev) => ({
...prev,
[id]: textAreaValue,
}));
}, [textAreaValue, id, setCreateTheme, setCreateHint, errorText]);

useEffect(() => {
if (!isFocus || !textAreaRef.current) {
setErrorText("");
return;
}
textAreaRef.current.focus();
}, [isFocus]);

const handleTextAreaChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
const cur = e.target.value;
const error = checkErrorText ? checkErrorText(cur) : "";
if (error) {
setErrorText(error);
setTextAreaValue(textAreaValue);
return;
}
setErrorText("");
setTextAreaValue(cur);
};

const handleTextAreaBlur = (e: FocusEvent<HTMLTextAreaElement>) => {
if (
!e.relatedTarget ||
(e.relatedTarget.className !== "theme-info focus" &&
e.relatedTarget.className !== "theme-info error")
) {
setIsFocus(false);
return;
}
textAreaRef.current?.focus();
setIsFocus(true);
};

return {
textAreaValue,
isFocus,
setIsFocus,
errorText,
textAreaRef,
handleTextAreaChange,
handleTextAreaBlur,
};
};

export default useTextArea;
3 changes: 2 additions & 1 deletion app/admin/(components)/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export default function Sidebar(props: Props) {
const navigateToNewTheme = () => {
resetSelectedTheme();
router.push("/admin");
setDrawer({ ...drawer, isOpen: false });
};
const handleSelectTheme = (theme: Theme) => {
if (drawer.isOpen && !drawer.isSameHint) {
Expand Down Expand Up @@ -134,7 +135,7 @@ export default function Sidebar(props: Props) {
</button>
</li>
</ul>
{!status?.includes("SUBSCRIPTION") && (
{!(status?.replaceAll(`"`, "") === "SUBSCRIPTION") && (
<div className="sidebar__subscribe">
<p className="sidebar__subscribe-title">
구독하고 힌트에 사진을 추가해 보세요
Expand Down
11 changes: 11 additions & 0 deletions app/admin/(components)/ThemeDrawer/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import Image from "next/image";

import { useSelectedHint } from "@/components/atoms/selectedHint.atom";
import ThemeTextField from "@/(shared)/(ThemeTextField)/Container";
import ThemeTextArea from "@/(shared)/(ThemeTextArea)/Container";

import ThemeDrawerAnswer from "./ThemeDrawerAnswer";
import ThemeDrawerHint from "./ThemeDrawerHint";
import {
answerTextAreaProps,
codeTextFieldProps,
HintImageProps,
hintTextAreaProps,
rateTextFieldProps,
XImageProps,
} from "./consts/themeDrawerProps";
Expand Down Expand Up @@ -81,12 +84,20 @@ const ThemeDrawer = ({ hintType, handleHintCreate }: DrawerType) => {
images={hintImages}
setImages={setHintImages}
/>
<ThemeTextArea
{...hintTextAreaProps}
content={selectedHint.contents ? selectedHint.contents : ""}
/>

<ThemeDrawerAnswer
imageType={"answer"}
images={answerImages}
setImages={setAnswerImages}
/>
<ThemeTextArea
{...answerTextAreaProps}
content={selectedHint.answer ? selectedHint.answer : ""}
/>
</div>

{hintType === "Edit" ? (
Expand Down
28 changes: 11 additions & 17 deletions app/admin/(components)/ThemeDrawer/ThemeDrawerAnswer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ const ThemeDrawerAnswer = ({
handleFileInputClick,
handleFileInputChange,
handleAddImageBtnClick,
handleTextAreaChange,
deleteLocalImage,
deleteServerImage,
answerInputRef,
Expand Down Expand Up @@ -53,9 +52,9 @@ const ThemeDrawerAnswer = ({
}/3)`}
</button>
</div>
<div className="drawer-images">
{selectedHint?.answerImageUrlList?.map((src, idx) => (
<div className="drawer-image-box" key={src}>
{selectedHint?.answerImageUrlList?.map((src, idx) => (
<div className="drawer-images" key={src}>
<div className="drawer-image-box">
<img src={src} alt={`answer-preview-${src}`} />
<div
className="drawer-image-dimmed"
Expand All @@ -66,10 +65,12 @@ const ThemeDrawerAnswer = ({
</button>
</div>
</div>
))}
{images.length > 0 &&
images.map((file, index) => (
<div key={file.name} className="drawer-image-box">
</div>
))}
{images.length > 0 &&
images.map((file, index) => (
<div className="drawer-images" key={file.name}>
<div className="drawer-image-box">
<img
src={URL.createObjectURL(file)}
alt={`answer-preview-${index}`}
Expand All @@ -83,15 +84,8 @@ const ThemeDrawerAnswer = ({
</button>
</div>
</div>
))}
</div>

<textarea
className="drawer-content-textarea"
placeholder="정답 내용을 입력해 주세요."
onChange={handleTextAreaChange}
defaultValue={selectedHint.answer}
/>
</div>
))}
</div>
);
};
Expand Down
28 changes: 11 additions & 17 deletions app/admin/(components)/ThemeDrawer/ThemeDrawerHint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ const ThemeDrawerHint = ({
handleFileInputClick,
handleFileInputChange,
handleAddImageBtnClick,
handleTextAreaChange,
deleteLocalImage,
deleteServerImage,
hintInputRef,
Expand Down Expand Up @@ -53,9 +52,9 @@ const ThemeDrawerHint = ({
}/3)`}
</button>
</div>
<div className="drawer-images">
{selectedHint?.hintImageUrlList?.map((src, idx) => (
<div className="drawer-image-box" key={src}>
{selectedHint?.hintImageUrlList?.map((src, idx) => (
<div className="drawer-images" key={src}>
<div className="drawer-image-box">
<img src={src} alt={`hint-preview-${src}`} />
<div
className="drawer-image-dimmed"
Expand All @@ -66,10 +65,12 @@ const ThemeDrawerHint = ({
</button>
</div>
</div>
))}
{images.length > 0 &&
images.map((file, index) => (
<div key={file.name} className="drawer-image-box">
</div>
))}
{images.length > 0 &&
images.map((file, index) => (
<div className="drawer-images" key={file.name}>
<div className="drawer-image-box">
<img
src={URL.createObjectURL(file)}
alt={`hint-preview-${index}`}
Expand All @@ -83,15 +84,8 @@ const ThemeDrawerHint = ({
</button>
</div>
</div>
))}
</div>

<textarea
className="drawer-content-textarea"
placeholder="힌트 내용을 입력해 주세요."
onChange={handleTextAreaChange}
defaultValue={selectedHint.contents}
/>
</div>
))}
</div>
);
};
Expand Down
19 changes: 18 additions & 1 deletion app/admin/(components)/ThemeDrawer/consts/themeDrawerProps.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ThemeInfoTextFieldType } from "@/(shared)/(ThemeTextField)/TextFieldType";
import { ThemeInfoTextAreaType } from "@/(shared)/(ThemeTextArea)/TextAreaType";

import {
codeValidations,
Expand All @@ -18,7 +19,7 @@ export const codeTextFieldProps: ThemeInfoTextFieldType = {

export const rateTextFieldProps: ThemeInfoTextFieldType = {
id: "progress",
tabIndex: 1,
tabIndex: 2,
title: "문제 진행률(%)",
content: "",
infoText: "",
Expand All @@ -27,6 +28,22 @@ export const rateTextFieldProps: ThemeInfoTextFieldType = {
checkErrorText: progressValidations,
};

export const hintTextAreaProps: ThemeInfoTextAreaType = {
id: "contents",
tabIndex: 3,
content: "",
infoText: "",
textAreaPlaceholder: "힌트 내용을 입력해 주세요.",
};

export const answerTextAreaProps: ThemeInfoTextAreaType = {
id: "answer",
tabIndex: 4,
content: "",
infoText: "",
textAreaPlaceholder: "정답 내용을 입력해 주세요.",
};

export const XImageProps = {
src: "/images/svg/icon_X.svg",
alt: "x_button",
Expand Down
Loading

0 comments on commit a993812

Please sign in to comment.