Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat : complete progress page #88

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Binary file added public/SampleImg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
129 changes: 129 additions & 0 deletions src/components/Aside/Aside.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
aside {
position: sticky; /* Make the aside sticky */
top: 20px; /* Adjust this value to control when the sidebar becomes sticky */
width: 250px; /* Adjust the width as needed */
height: calc(100vh - 40px); /* Full viewport height minus top/bottom padding */
overflow-y: auto; /* Allow vertical scrolling inside the aside */
}

.cohort {
display: flex;
justify-content: flex-end;
}

.container {
top: 230px;
left: 20px;
width: 250px;
height: 550px;
padding: 20px 35px;
background-color: var(--bg-200);
border-radius: 10px;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
}

.profile {
position: relative;
text-align: center;
margin-bottom: 25px;
}

.avatar {
position: relative;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 10px;
}

.avatar img {
position: absolute;
width: 70px;
height: 70px;
border-radius: 50%;
}

.progress-circle {
width: 90px;
height: 90px;
border-radius: 50%;
background: conic-gradient(
from 180deg,
var(--secondary) 0%,
var(--primary) var(--progress),
var(--bg-200) var(--progress)
);
}

.progress-text {
position: absolute;
bottom: -20px;
left: 50%;
transform: translateX(-50%);
font-size: 14px;
font-weight: bold;
color: var(--text-secondary);
}

.username {
font-size: 14px;
color: var(--text-secondary);
}

.currentStatus {
display: flex;
justify-content: center;
margin-bottom: 35px;
}

.currentStatus img {
width: 64px;
height: 64px;
}

.taskCounts {
display: flex;
flex-direction: column;
justify-content: center;
gap: 35px;
margin-bottom: 35px;
}

.task {
display: flex;
flex-direction: column;
align-items: center;
font-size: 14px;
font-weight: bold;
gap: 5px;
}

.easy {
color: #1cbaba;
}

.medium {
color: #ffb700;
}

.hard {
color: #f63737;
}

.buttonLink {
display: block;
width: 100%;
padding: 10px 0;
background-color: var(--bg-200);
border: 1px solid var(--primary);
border-radius: 10px;
color: var(--primary);
font-size: 14px;
font-weight: bold;
text-align: center;
}

.returnButton:hover {
background-color: var(--primary);
color: var(--bg-100);
}
47 changes: 47 additions & 0 deletions src/components/Aside/Aside.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { Meta, StoryObj } from "@storybook/react";
import Aside from "./Aside";
import { Grade } from "../../api/services/common/types.ts";

const meta = {
component: Aside,
} satisfies Meta<typeof Aside>;

export default meta;

export const Default: StoryObj<typeof meta> = {
args: {
githubUsername: "testuser",
easyTasks: "5/10",
mediumTasks: "8/10",
hardTasks: "3/5",
solvedTasks: 16,
totalTasks: 25,
profile_url: "https://example.com/profile.jpg", // Provide a valid URL or mock
cohort: 3,
grade: Grade.SMALL_TREE, // Assign an appropriate grade here
},
};

export const HighProgress: StoryObj<typeof meta> = {
args: {
...Default.args,
solvedTasks: 28,
totalTasks: 30,
easyTasks: "10/10",
mediumTasks: "10/10",
hardTasks: "8/10",
grade: Grade.BIG_TREE, // Higher grade for more progress
},
};

export const NoTasks: StoryObj<typeof meta> = {
args: {
...Default.args,
easyTasks: "0/0",
mediumTasks: "0/0",
hardTasks: "0/0",
solvedTasks: 0,
totalTasks: 0,
grade: Grade.SEED, // Minimal grade due to no tasks solved
},
};
63 changes: 63 additions & 0 deletions src/components/Aside/Aside.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { test, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import Aside from "./Aside";
import { Grade } from "../../api/services/common/types.ts";

test("renders the Aside component", () => {
render(
<Aside
githubUsername="testuser"
easyTasks="10/15"
mediumTasks="5/10"
hardTasks="2/5"
solvedTasks={17}
totalTasks={30}
profile_url="https://example.com/profile.jpg"
cohort={3}
grade={Grade.SMALL_TREE}
/>,
);

const aside = screen.getByRole("complementary");
expect(aside).toBeInTheDocument();
});

test("displays the username", () => {
render(
<Aside
githubUsername="testuser"
easyTasks="10/15"
mediumTasks="5/10"
hardTasks="2/5"
solvedTasks={17}
totalTasks={30}
profile_url="https://example.com/profile.jpg"
cohort={3}
grade={Grade.SMALL_TREE}
/>,
);

const username = screen.getByText(/testuser/i);
expect(username).toBeInTheDocument();
});

test("renders the button link with the text", () => {
render(
<Aside
githubUsername="testuser"
easyTasks="10/15"
mediumTasks="5/10"
hardTasks="2/5"
solvedTasks={17}
totalTasks={30}
profile_url="https://example.com/profile.jpg"
cohort={3}
grade={Grade.SMALL_TREE}
/>,
);

const buttonLink = screen.getByRole("link", {
name: /리더보드로 돌아가기/i,
});
expect(buttonLink).toBeInTheDocument();
});
92 changes: 92 additions & 0 deletions src/components/Aside/Aside.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useEffect } from "react";
import styles from "./Aside.module.css";
import Seed from "../../assets/Seed.png";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assets 디렉토리에 이미지 파일이 없는 것 같은데요?

Shot 2024-11-23 at 16 29 24

Copy link
Contributor Author

@SamTheKorean SamTheKorean Nov 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 드래프트로 올려놓고 다른 pr 마지되면 rebase해서 가져와서 올리고 pr 오픈할 예정이었습니다..! 여기에 올리면 충돌을 해결해야할 것 같아서요.! 생각해보니 같은 파일이라 충돌은 안날 수 있겠네요..! 떠나서 다른 분들에게 혼란을 드리는 것 같으니 이제는 충돌이 예상되더라도 우선 올리고 해결하겠습니다!

import Sprout from "../../assets/Sprout.png";
import YoungTree from "../../assets/YoungTree.png";
import LargeTree from "../../assets/LargeTree.png";
import { Grade } from "../../api/services/common/types.ts";

interface AsideProps {
githubUsername: string;
easyTasks: string;
mediumTasks: string;
hardTasks: string;
solvedTasks: number;
totalTasks: number;
profile_url: string;
cohort: number;
grade: Grade;
}

const imageTable = {
SEED: Seed,
SPROUT: Sprout,
SMALL_TREE: YoungTree,
BIG_TREE: LargeTree,
};


export default function Aside({
githubUsername,
easyTasks,
mediumTasks,
hardTasks,
solvedTasks,
totalTasks,
profile_url,
cohort,
grade,
}: AsideProps) {
const progressPercent = Math.min((solvedTasks / totalTasks) * 100, 100);

useEffect(() => {
document.documentElement.style.setProperty(
"--progress",
`${progressPercent}%`,
);
}, [progressPercent]);

return (
<aside >
<div className={styles.container}>
<span className={styles.cohort}>{cohort}</span>
<section className={styles.profile}>
<div className={styles.avatar}>
<div className={styles["progress-circle"]}></div>
<img src={profile_url} alt="User's profile picture" />
</div>
<div className={styles.progress}>
<a href={`https://github.com/DaleStudy/leetcode-study/pulls?q=is%3Apr+author%3A${githubUsername}`}>
풀이 보기
</a>
</div>
<p className={styles.username}>{githubUsername}</p>
</section>

<section className={styles.currentStatus}>
<figure>
<img src={imageTable[grade]} alt={`${grade} 등급`} />
</figure>
</section>
<section className={styles.taskCounts}>
<div className={styles.task}>
<span className={styles.easy}>EASY</span>
<span>{easyTasks}</span>
</div>
<div className={styles.task}>
<span className={styles.medium}>MEDIUM</span>
<span>{mediumTasks}</span>
</div>
<div className={styles.task}>
<span className={styles.hard}>HARD</span>
<span>{hardTasks}</span>
</div>
</section>
</div>
<a href="../Leaderboard/Leaderboard.tsx" className={styles.buttonLink}>
리더보드로 돌아가기
</a>
</aside>
);
}

1 change: 1 addition & 0 deletions src/components/Progress/Progress.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@
}

.problemList {
margin-top: 77px;
width: 80%;
}
48 changes: 13 additions & 35 deletions src/components/Progress/Progress.test.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,19 @@
import { beforeEach, describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import Progress from "./Progress"; // Adjust the import path as needed
import { render, screen, waitFor } from "@testing-library/react";
import Progress from "./Progress";
import { mock } from "vitest-mock-extended";
import useMembers from "../../hooks/useMembers";
import { test, vi } from "vitest";

describe("<Progress/>", () => {
beforeEach(() => render(<Progress />));
vi.mock("../../hooks/useMembers");

it("renders the table", () => {
const table = screen.getByRole("table");
expect(table).toBeInTheDocument();
});
test("render the site header", () => {
vi.mocked(useMembers).mockReturnValue(
mock({ isLoading: false, error: null, members: [] }),
);

it("renders the page header", () => {
const header = screen.getByRole("banner");
expect(header).toBeInTheDocument();
});
render(<Progress />);

it("renders the title", () => {
const heading = screen.getByRole("heading", { level: 1 });
expect(heading).toHaveTextContent("Progress");
});

it("renders the profile information", () => {
const profileSection = screen.getByRole("heading", {
level: 2,
name: /Profile Section/i,
});
expect(profileSection).toBeInTheDocument();

expect(screen.getByRole("heading", { level: 3 })).toHaveTextContent(
"0 Attempting",
);
expect(screen.getByText(/^Easy: \d{1,2}\/\d{1,2}$/)).toBeInTheDocument();
expect(screen.getByText(/^Med.: \d{1,2}\/\d{1,2}$/)).toBeInTheDocument();
expect(screen.getByText(/^Hard: \d{1,2}\/\d{1,2}$/)).toBeInTheDocument();
});

it("renders footer", () => {
expect(screen.getByRole("contentinfo"));
});
const header = screen.getByRole("banner");
expect(header).toBeInTheDocument();
});
Loading