Skip to content

Commit

Permalink
Merge pull request #123 from softeerbootcamp4th/fix/103-ux-improve
Browse files Browse the repository at this point in the history
[modify] 로그아웃 기능 추가, 자잘한 버그 수정, 접근성 개선
  • Loading branch information
darkdulgi authored Aug 20, 2024
2 parents 94efa4f + b063d9f commit 77f1546
Show file tree
Hide file tree
Showing 42 changed files with 502 additions and 228 deletions.
3 changes: 1 addition & 2 deletions src/adminPage/features/eventEdit/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ function EventEditor({ initialData = null } = {}) {
title={`${mode === "create" ? "등록" : "수정"} 완료`}
description={`이벤트가 성공적으로 ${mode === "create" ? "등록" : "수정"}되었습니다!`}
/>,
);
navigate(mode === "create" ? "/events" : `/events/${state.eventId}`);
).then(() => navigate(mode === "create" ? "/events" : `/events/${state.eventId}`));
},
onError: (e) => {
openModal(<AlertModal title="등록 실패" description={e.message} />);
Expand Down
2 changes: 1 addition & 1 deletion src/common/constants.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const EVENT_FCFS_ID = 1;
export const EVENT_FCFS_ID = "HD_240808_001";
export const EVENT_DRAW_ID = "HD-19700101-01";
export const EVENT_ID = "the-new-ioniq5";
export const EVENT_START_DATE = new Date(2024, 8, 9);
Expand Down
24 changes: 13 additions & 11 deletions src/common/dataFetch/getQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,18 +103,20 @@ export function useQuery(key, promiseFn, config = {}) {
*
* @return Function : 호출 시, 실제로 post 요청을 발송하는 함수를 반환합니다.
*/
export async function mutate(key, promiseFn, { onSuccess, onError } = {}) {
try {
const value = await promiseFn();
updateSubscribedQuery(key);
onSuccess?.(value);
return value;
} catch (e) {
onError?.(e);
if (onError === undefined) throw e;
}
}

export function useMutation(key, promiseFn, { onSuccess, onError } = {}) {
return async () => {
try {
const value = await promiseFn();
updateSubscribedQuery(key);
onSuccess?.(value);
return value;
} catch (e) {
onError?.(e);
if (onError === undefined) throw e;
}
};
return () => mutate(key, promiseFn, { onSuccess, onError });
}

export function getQuerySuspense(key, promiseFn, dependencyArray = []) {
Expand Down
16 changes: 16 additions & 0 deletions src/common/modal/modal.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createContext, useCallback, useEffect, useState, useRef } from "react";
import useModalStore, { closeModal } from "./store.js";
import useFocusTrap from "./useFocusTrap.js";

export const ModalCloseContext = createContext(() => {
console.log("모달이 닫힙니다.");
Expand Down Expand Up @@ -29,11 +30,26 @@ function Modal({ layer }) {
}
}, [child]);

useEffect(() => {
if (child === null) return;

function escHatch(e) {
if (e.key !== "Escape") return;
close();
e.preventDefault();
}
document.addEventListener("keydown", escHatch);
return () => document.removeEventListener("keydown", escHatch);
}, [child, close]);

const focusTrapRef = useFocusTrap(child !== null);

return (
<ModalCloseContext.Provider value={close}>
{child !== null ? (
<div
className={`fixed z-[100] top-0 left-0 w-full h-dvh flex justify-center items-center transition-opacity ${opacity === 0 ? "opacity-0" : "opacity-100"}`}
ref={focusTrapRef}
>
{child}
<div
Expand Down
9 changes: 9 additions & 0 deletions src/common/modal/openModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,13 @@ import { modalStore } from "./store.js";

export default function openModal(component, layer = "alert") {
modalStore.changeModal(component, layer);
return new Promise((resolve) => {
function observe() {
if (modalStore.getSnapshot(layer) !== component) {
resolve();
clear();
}
}
const clear = modalStore.subscribe(observe);
});
}
64 changes: 64 additions & 0 deletions src/common/modal/useFocusTrap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useEffect, useRef } from "react";

function getEndPointChild(element) {
const focusableElements = [
...element.querySelectorAll(
"a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]",
),
].filter((elem) => elem.tabIndex >= 0);
if (focusableElements.length === 0) return [null, null];
return [focusableElements[0], focusableElements[focusableElements.length - 1]];
}

function useFocusTrap(active) {
const prevRef = useRef(null);
const ref = useRef(null);
const endPointChild = useRef([null, null]);
useEffect(() => {
if (!active || ref.current === null) return;

function renewEndPointChild() {
if (ref.current === null) return;
endPointChild.current = getEndPointChild(ref.current);
}

function handleTabKey(e) {
if (e.key !== "Tab") return;

const [first, last] = endPointChild.current;

if (document.activeElement === prevRef.current) {
if (e.shiftKey) last?.focus();
else first?.focus();
e.preventDefault();
return;
}

if (first === null || last === null) return;
if (document.activeElement === last && !e.shiftKey) {
first.focus();
e.preventDefault();
} else if (document.activeElement === first && e.shiftKey) {
last.focus();
e.preventDefault();
}
}

renewEndPointChild();
prevRef.current = document.activeElement;
document.addEventListener("keydown", handleTabKey);
const config = { subtree: true, childList: true, attributeFilter: ["disabled", "tabindex"] };
const observer = new MutationObserver(renewEndPointChild);
observer.observe(ref.current, config);

return () => {
document.removeEventListener("keydown", handleTabKey);
observer.disconnect();
prevRef.current.focus();
};
}, [active]);

return ref;
}

export default useFocusTrap;
13 changes: 9 additions & 4 deletions src/mainPage/features/comment/autoScrollCarousel/index.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import useAutoCarousel from "./useAutoCarousel.js";

function AutoScrollCarousel({ speed = 1, gap = 0, children }) {
const { position, ref, eventListener } = useAutoCarousel(speed);
const { position, ref, eventListener } = useAutoCarousel(speed, gap);

const flexStyle = "flex [&>div]:flex-shrink-0 gap-[var(--gap,0)] items-center absolute";
const flexStyle =
"min-w-full flex [&>div]:flex-shrink-0 gap-[var(--gap,0)] justify-around items-center absolute";
return (
<div className="w-full h-full overflow-hidden" {...eventListener}>
<div
Expand All @@ -13,11 +14,15 @@ function AutoScrollCarousel({ speed = 1, gap = 0, children }) {
}}
className="relative h-max touch-pan-y"
>
<div className={`${flexStyle} -translate-x-[calc(100%+var(--gap,0px))]`}>{children}</div>
<div className={`${flexStyle} -translate-x-[calc(100%+var(--gap,0px))]`} aria-hidden="true">
{children}
</div>
<div className={flexStyle} ref={ref}>
{children}
</div>
<div className={`${flexStyle} translate-x-[calc(100%+var(--gap,0px))]`}>{children}</div>
<div className={`${flexStyle} translate-x-[calc(100%+var(--gap,0px))]`} aria-hidden="true">
{children}
</div>
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const FRICTION_RATE = 0.1;
const MOMENTUM_THRESHOLD = 0.6;
const MOMENTUM_RATE = 0.3;

function useAutoCarousel(speed = 1) {
function useAutoCarousel(speed = 1, gap = 0) {
const childRef = useRef(null);
const [position, setPosition] = useState(0);
const [isControlled, setIsControlled] = useState(false);
Expand All @@ -19,7 +19,7 @@ function useAutoCarousel(speed = 1) {
(time) => {
if (childRef.current === null) return;

const width = childRef.current.clientWidth;
const width = childRef.current.clientWidth + gap;

// 마우스 뗐을 때 관성 재계산
const baseSpeed = isHovered ? 0 : speed;
Expand All @@ -38,7 +38,7 @@ function useAutoCarousel(speed = 1) {
// 타임스탬프 저장
timestamp.current = time;
},
[isHovered, speed],
[isHovered, speed, gap],
);

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useQuery } from "@common/dataFetch/getQuery.js";
import { fetchServer } from "@common/dataFetch/fetchServer.js";
import CommentCarouselNoData from "./CommentCarouselNoData.jsx";
import AutoScrollCarousel from "../autoScrollCarousel";
import { formatDate } from "@common/utils.js";
import { EVENT_ID } from "@common/constants.js";
Expand All @@ -14,6 +15,8 @@ function mask(string) {
function CommentCarousel() {
const { comments } = useQuery("comment-data", () => fetchServer(`/api/v1/comment/${EVENT_ID}`));

if (comments.length === 0) return <CommentCarouselNoData />;

return (
<div className="w-full h-[29rem]">
<AutoScrollCarousel speed={0.1} gap={28}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
function CommentCarouselNoData() {
return (
<div className="w-full h-[29rem] flex justify-center items-center px-6">
<div className="w-full max-w-[1200px] h-96 bg-neutral-50 flex flex-col justify-center items-center gap-4">
<img src="/icons/error.svg" alt="기대평 없음" width="120" height="120" />
<p className="text-body-l text-red-500 font-bold">기대평이 없어요!</p>
</div>
</div>
);
}

export default CommentCarouselNoData;
14 changes: 6 additions & 8 deletions src/mainPage/features/comment/modals/CommentNegativeModal.jsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import { useContext } from "react";
import { ModalCloseContext } from "@common/modal/modal.jsx";
import AlertModalContainer from "@main/components/AlertModalContainer.jsx";
import Button from "@common/components/Button.jsx";

function CommentNegativeModal() {
const close = useContext(ModalCloseContext);

return (
<div className="w-[calc(100%-1rem)] max-w-[31.25rem] h-[calc(100svh-2rem)] max-h-[15.625rem] p-10 shadow bg-white relative flex flex-col justify-between items-center">
<div className="flex flex-col gap-2 items-center">
<p className="text-body-l font-bold text-neutral-700">해당 기대평을 등록할 수 없습니다</p>
<p className="w-full max-w-80 text-body-s font-medium text-neutral-400 text-center">
비속어, 혐오표현 등 타인에게 불쾌감을 줄 수 있는 표현이 포함된 기대평은 작성이 불가합니다
</p>
</div>
<AlertModalContainer
title="해당 기대평을 등록할 수 없습니다"
description="비속어, 혐오표현 등 타인에게 불쾌감을 줄 수 있는 표현이 포함된 기대평은 작성이 불가합니다"
>
<Button styleType="filled" onClick={close}>
확인
</Button>
</div>
</AlertModalContainer>
);
}

Expand Down
18 changes: 8 additions & 10 deletions src/mainPage/features/comment/modals/CommentNoUserModal.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useContext } from "react";
import { ModalCloseContext } from "@common/modal/modal.jsx";
import AlertModalContainer from "@main/components/AlertModalContainer.jsx";
import Button from "@common/components/Button.jsx";
import scrollTo from "@main/scroll/scrollTo.js";
import { INTERACTION_SECTION } from "@main/scroll/constants.js";
Expand All @@ -13,22 +14,19 @@ function CommentNoUserModal() {
}

return (
<div className="w-[calc(100%-1rem)] max-w-[31.25rem] h-[calc(100svh-2rem)] max-h-[31.25rem] p-10 shadow bg-white relative flex flex-col justify-between items-center">
<div className="flex flex-col gap-2 items-center">
<p className="text-body-l font-bold text-neutral-700">아직 기대평을 작성할 수 없습니다.</p>
<p className="w-full max-w-80 text-body-s font-medium text-neutral-400 text-center">
오늘의 추첨 이벤트에 참여하고 기대평을 작성하세요
</p>
</div>
<div className="absolute top-0 left-0 w-full h-full flex justify-center items-center pointer-events-none">
<AlertModalContainer
title="아직 기대평을 작성할 수 없습니다."
description="오늘의 추첨 이벤트에 참여하고 기대평을 작성하세요"
image={
<img
src="/icons/[email protected]"
srcSet="/icons/[email protected] 1x, /icons/[email protected] 2x"
alt="추첨 이벤트 참여 바랍니다"
width="208"
height="40"
/>
</div>
}
>
<div className="w-full flex flex-wrap justify-center gap-5">
<Button styleType="filled" onClick={toMoveInteraction}>
추첨 이벤트 참여하기
Expand All @@ -37,7 +35,7 @@ function CommentNoUserModal() {
닫기
</Button>
</div>
</div>
</AlertModalContainer>
);
}

Expand Down
12 changes: 7 additions & 5 deletions src/mainPage/features/comment/modals/CommentSuccessModal.jsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
import { useContext } from "react";
import { ModalCloseContext } from "@common/modal/modal.jsx";
import AlertModalContainer from "@main/components/AlertModalContainer.jsx";
import Button from "@common/components/Button.jsx";

function CommentSuccessModal() {
const close = useContext(ModalCloseContext);

return (
<div className="w-[calc(100%-1rem)] max-w-[31.25rem] h-[calc(100svh-2rem)] max-h-[31.25rem] p-10 shadow bg-white relative flex flex-col justify-between items-center">
<p className="text-body-l font-bold text-neutral-700">기대평이 등록되었습니다!</p>
<div className="absolute top-0 left-0 w-full h-full flex justify-center items-center pointer-events-none">
<AlertModalContainer
title="기대평이 등록되었습니다!"
image={
<img
src="/icons/[email protected]"
srcSet="/icons/[email protected] 1x, /icons/[email protected] 2x"
alt="기대평 등록 완료"
width="173"
height="182"
/>
</div>
}
>
<Button styleType="ghost" onClick={close}>
확인
</Button>
</div>
</AlertModalContainer>
);
}

Expand Down
Loading

0 comments on commit 77f1546

Please sign in to comment.