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
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ export default function GoalSkeleton() {
<div className={`relative animate-pulse ${cardStyles}`}>
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-2">
<div className="h-5 w-40 rounded bg-gray-200" />
<div className="h-2 w-56 rounded bg-gray-200" />
<div className="bg-gray-80 h-5 w-40 rounded" />
<div className="bg-gray-80 h-2 w-56 rounded" />
</div>

<div className="flex items-center gap-2 sm:static">
<div className="absolute top-7.5 right-5 h-6 w-15 rounded-full bg-gray-200 sm:h-8 sm:w-20" />
<div className="absolute bottom-0 left-1/2 h-8 w-8 -translate-x-1/2 rounded-full bg-gray-200 sm:static" />
<div className="bg-gray-80 absolute top-7.5 right-5 h-6 w-15 rounded-full sm:relative sm:top-0 sm:h-8 sm:w-20" />
<div className="bg-gray-80 absolute bottom-0 left-1/2 h-8 w-8 -translate-x-1/2 rounded-full sm:static" />
</div>
</div>

Expand All @@ -33,7 +33,6 @@ export default function GoalSkeleton() {
title="DONE"
variant="done">
<ListSkeleton
variant="light"
count={2}
rowClassName="h-10 bg-white/40"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import ProgressCardSkeleton from "@/components/skeleton/ProgressCardSkeleton";
import ProgressContent from "./ProgressContent";
import { useAuthStore } from "@/store/useAuthStore";
import { AsyncBoundary } from "@/app/(protected)/_components/AsyncBoundary";
import { FallbackProps } from "react-error-boundary";
import Button from "@/components/common/button/Button";

export default function ProgressTodos() {
const user = useAuthStore((state) => state.user);
Expand All @@ -22,7 +24,20 @@ export default function ProgressTodos() {

<div className="h-46.5 rounded-[28px] bg-blue-200 bg-[url(/images/dashboard/obj-progress.png)] bg-size-[151px_auto] bg-position-[right_8px_bottom_-54px] bg-no-repeat shadow-[0_10px_40px_0_rgba(0,212,190,0.24)] transition-all sm:h-53 sm:bg-position-[right_-24px_bottom_-54px] lg:h-64 lg:rounded-[40px] lg:bg-size-[222px_auto] lg:bg-position-[right_-4px_bottom_-45px] lg:hover:shadow-[0_10px_40px_0_rgba(0,212,190,0.24)]">
<div className="flex h-full items-center justify-start gap-5 pl-6 lg:pl-10 xl:justify-center xl:gap-3 xl:pl-0 2xl:justify-start 2xl:gap-6 2xl:pl-12">
<AsyncBoundary loadingFallback={<ProgressCardSkeleton />}>
<AsyncBoundary
loadingFallback={<ProgressCardSkeleton />}
errorFallback={({ error, resetErrorBoundary }: FallbackProps) => (
<div className="-ml-6 flex h-full w-full flex-col items-center justify-center">
<p className="mb-8 text-sm font-medium text-gray-600 sm:text-base">
{error.message}
</p>
<Button
size="compact"
onClick={resetErrorBoundary}>
λ‹€μ‹œ μ‹œλ„
</Button>
</div>
)}>
<ProgressContent nickname={nickname} />
</AsyncBoundary>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { ChevronRightIcon } from "@heroicons/react/24/outline";
import { ListSkeleton } from "@/components/skeleton/ListSkeleton";
import RecentTodosContent from "./RecentTodosContent";
import { AsyncBoundary } from "@/app/(protected)/_components/AsyncBoundary";
import { FallbackProps } from "react-error-boundary";
import Button from "@/components/common/button/Button";

export default function RecentTodos() {
return (
Expand All @@ -30,9 +32,22 @@ export default function RecentTodos() {
loadingFallback={
<ListSkeleton
count={4}
rowClassName="h-10 lg:h-11 bg-white/40"
rowClassName="h-10 lg:h-11"
/>
}>
}
errorFallback={({ error, resetErrorBoundary }: FallbackProps) => (
<div className="flex h-full flex-col items-center justify-center">
<p className="mb-8 text-sm font-medium text-gray-600 sm:text-base">
{error.message}
</p>
<Button
size="compact"
onClick={resetErrorBoundary}
className={"bg-white text-black hover:text-white"}>
λ‹€μ‹œ μ‹œλ„
</Button>
</div>
)}>
<RecentTodosContent />
</AsyncBoundary>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/app/(protected)/goals/[goalId]/_components/Goal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default function Goal({ goalTodos }: { goalTodos: ListTodoType[] }) {

const goalTodoChecked = goalTodos.filter((goalTodo) => !goalTodo.checked);
const goalTodoCheckedDone = goalTodos.filter((goalTodo) => goalTodo.checked);

return (
<section className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-2">
<GoalSection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Goal from "./Goal";
import { calcProgress } from "../_utils/calcProgress";
import { useTodosSuspense } from "@/hooks/queries/todos/useTodosSuspense";
import { useGoalListSuspense } from "@/hooks/queries/goals/useGoalListSuspense";
import { useEffect } from "react";

type DataIdProps = {
goalId: string;
Expand All @@ -23,6 +24,24 @@ export default function GoalContainerData({ goalId }: DataIdProps) {
const progress = calcProgress(goalTodos);

const targetTodoId = goalTodos.length > 0 ? goalTodos[0].id : null;
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= 640) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
};

handleResize();

window.addEventListener("resize", handleResize);

return () => {
window.removeEventListener("resize", handleResize);
document.body.style.overflow = "";
};
}, []);
return (
<>
<div className="mb-7.5 block w-full sm:grid lg:mb-20 lg:grid-cols-1 lg:gap-x-0 xl:gap-x-8 2xl:grid-cols-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,27 @@ import { ListSkeleton } from "@/components/skeleton/ListSkeleton";
export default function GoalContainerSkeleton() {
return (
<section className="animate-skeleton-fade">
<div className="mb-8.5">
<div className="h-8 w-32 rounded bg-gray-200" />
</div>

<div className="mb-7.5 block w-full sm:grid lg:mb-20 lg:grid-cols-1 lg:gap-x-0 xl:gap-x-8 2xl:grid-cols-2">
<div className="mb-4 rounded-[28px] bg-white p-4 shadow lg:rounded-[40px] lg:p-6">
<div className="flex items-center justify-between">
<div className="h-6 w-48 rounded bg-gray-200" />
<div className="h-8 w-8 rounded-full bg-gray-200" />
<div className="bg-gray-80 h-6 w-48 rounded" />
<div className="bg-gray-80 h-8 w-8 rounded-full" />
</div>
</div>

<div className="block sm:grid sm:grid-cols-2 sm:gap-x-5 lg:mt-5 lg:gap-x-5 xl:mt-5 xl:grid xl:grid-cols-2 2xl:mt-0">
<div className="mb-4 rounded-[28px] bg-white p-6 shadow sm:mb-0 lg:rounded-[40px]">
<div className="flex flex-col items-center justify-center gap-4">
<div className="h-24 w-24 rounded-full bg-gray-200" />
<div className="h-4 w-20 rounded bg-gray-200" />
<div className="bg-gray-80 h-24 w-24 rounded-full" />
<div className="bg-gray-80 h-4 w-20 rounded" />
</div>
</div>

<div className="rounded-[28px] bg-white p-6 shadow lg:rounded-[40px]">
<div className="flex flex-col gap-3">
<div className="h-5 w-24 rounded bg-gray-200" />
<div className="h-4 w-full rounded bg-gray-200" />
<div className="h-4 w-3/4 rounded bg-gray-200" />
<div className="bg-gray-80 h-5 w-24 rounded" />
<div className="bg-gray-80 h-4 w-full rounded" />
<div className="bg-gray-80 h-4 w-3/4 rounded" />
</div>
</div>
</div>
Expand All @@ -36,6 +32,7 @@ export default function GoalContainerSkeleton() {
<div className="rounded-[28px] bg-white p-4 shadow lg:rounded-[40px] lg:p-6">
<ListSkeleton
count={5}
variant="light"
rowClassName="h-10 lg:h-11"
/>
</div>
Expand Down
24 changes: 19 additions & 5 deletions src/app/(protected)/goals/[goalId]/_components/GoalHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useDeleteGoalMutation } from "@/hooks/queries/goals/useDeleteGoalMutati
import ConfirmModal from "@/components/common/popup-modal/ConfirmModal";
import { useRouter } from "next/navigation";
import { toast } from "@/lib/toast";
import BaseInput from "@/components/common/input/base-input/BaseInput";

type GoalHeaderProps = {
goalId: string;
Expand Down Expand Up @@ -41,7 +42,7 @@ export default function GoalHeader({ goalId }: GoalHeaderProps) {
text: "μˆ˜μ •ν•˜κΈ°",
onClick: () => {
closeDropdown();
setEditTitle(goal?.title ?? "");
setEditTitle("");
setIsEditing(true);
},
},
Expand All @@ -66,6 +67,11 @@ export default function GoalHeader({ goalId }: GoalHeaderProps) {
);
};

const handleCancelEdit = () => {
setEditTitle("");
setIsEditing(false);
};

const handleConfim = () => {
deleteGoal(numericGoalId, {
onSuccess: () => {
Expand All @@ -86,22 +92,30 @@ export default function GoalHeader({ goalId }: GoalHeaderProps) {
<h3 className="truncate text-base font-semibold">{goal?.title}</h3>
) : (
<div className="flex w-full gap-2">
<input
type="text"
className="w-[70%] rounded border p-2 sm:w-[80%]"
<BaseInput
id="goal-title-edit"
className="w-[70%] sm:w-[80%]"
value={editTitle}
type="text"
onChange={(e) => setEditTitle(e.target.value)}
placeholder="μˆ˜μ •ν•  λͺ©ν‘œλ₯Ό μ μ–΄μ£Όμ„Έμš”."
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleSave();
}
}}
/>

<TextButton
onClick={handleSave}
className="w-[20%] sm:w-[10%]">
μˆ˜μ • μ™„λ£Œ
μˆ˜μ •
</TextButton>
<TextButton
onClick={handleCancelEdit}
className="w-[20%] text-black sm:w-[10%]">
μ·¨μ†Œ
</TextButton>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default function GoalSection({
className="grid sm:gap-0.5 lg:gap-1"
items={items}
onToggleChecked={onToggleChecked}
containerClassName="sm:h-110"
containerClassName="sm:h-110 overflow-hidden overflow-y-auto"
/>
)}
</div>
Expand Down
3 changes: 3 additions & 0 deletions src/components/common/input/base-input/BaseInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface BaseInputProps {
id?: string;
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
placeholder?: string;
type?: InputType;
className?: string;
Expand All @@ -20,6 +21,7 @@ export default function BaseInput({
id,
value,
onChange,
onKeyDown,
placeholder = "",
type = "text",
className,
Expand All @@ -46,6 +48,7 @@ export default function BaseInput({
value={value}
placeholder={placeholder}
onChange={onChange}
onKeyDown={onKeyDown}
className={inputClassName}
/>
{rightIcon}
Expand Down
22 changes: 21 additions & 1 deletion tests/unit/goals/goals.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,26 @@ describe("λͺ©ν‘œ μ˜μ—­", () => {
expect(screen.getByText("μˆ˜μ •ν•˜κΈ°")).toBeInTheDocument();
expect(screen.getByText("μ‚­μ œν•˜κΈ°")).toBeInTheDocument();
});
it("λͺ©ν‘œ μˆ˜μ • 쀑 μ·¨μ†Œλ₯Ό λˆ„λ₯΄λ©΄ μˆ˜μ • λͺ¨λ“œκ°€ μ’…λ£Œλœλ‹€", async () => {
const user = userEvent.setup();

renderWithQueryClient(<GoalHeader goalId="1" />);

await user.click(screen.getByLabelText("goal-options"));
await user.click(screen.getByText("μˆ˜μ •ν•˜κΈ°"));

const input = screen.getByRole("textbox");
expect(input).toBeInTheDocument();

await user.type(input, "할일");

await user.click(screen.getByText("μ·¨μ†Œ"));

expect(screen.queryByRole("textbox")).not.toBeInTheDocument();

expect(updateGoal).not.toHaveBeenCalled();
});

it("λͺ©ν‘œλ₯Ό μˆ˜μ •ν•˜λ©΄ μž…λ ₯ν•œ κ°’μœΌλ‘œ λ³€κ²½λœλ‹€", async () => {
const user = userEvent.setup();
(updateGoal as jest.Mock).mockResolvedValueOnce({
Expand All @@ -53,7 +73,7 @@ describe("λͺ©ν‘œ μ˜μ—­", () => {
await user.clear(input);
await user.type(input, "μƒˆ λͺ©ν‘œ");

await user.click(screen.getByText("μˆ˜μ • μ™„λ£Œ"));
await user.click(screen.getByText("μˆ˜μ •"));

expect(await screen.findByText("μƒˆ λͺ©ν‘œ")).toBeInTheDocument();

Expand Down