GitHub - fc-micro-frontends/career-up at step10
REACT_APP_AUTH0_DOMAIN=dev-vcrmf0xuep020tri.us.auth0.com
REACT_APP_AUTH0_CLIENT_ID=27LLb4I9cIjiiQNSjbNIX9sQM1KECUfk
REACT_APP_AUTH0_CALLBACK_URL=http://localhost:3000
➜ pnpm i
➜ pnpm build
// career-up/apps/edu/src/types.ts
import { type User } from "@auth0/auth0-spa-js";
export interface UserType extends User {
view_count: number;
update_count: number;
courses: { courseId: number; done: false }[];
}
export interface CourseType {
id: number;
thumbnail: string;
title: string;
description: string;
}
export interface CourseContentsType {
id: number;
goals: string[];
summaries: string[];
}
// career-up/apps/edu/src/apis.ts
import {
type CourseContentsType,
type CourseType,
type UserType,
} from "./types";
export async function getCourses(token: string): Promise<CourseType[]> {
const res = await fetch(
"<http://localhost:4000/courses?_sort=id&_order=desc>",
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
return await res.json();
}
export async function getCourseContents(
token: string,
id: number
): Promise<CourseContentsType> {
const res = await fetch(`http://localhost:4000/course-contents/${id}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return await res.json();
}
export async function getUser(token: string): Promise<UserType> {
const res = await fetch("<http://localhost:4000/user>", {
headers: {
Authorization: `Bearer ${token}`,
},
});
return await res.json();
}
// career-up/apps/edu/src/routes.tsx
import React from "react";
import { type RouteObject } from "react-router-dom";
import { AppRoutingManager } from "@career-up/shell-router";
import Auth0ClientProvider from "./providers/auth0-client-provider";
**import Layout from "./components/layout";**
export const routes: RouteObject[] = [
{
path: "/",
element: (
<Auth0ClientProvider>
**<Layout>**
<AppRoutingManager type="app-edu" />
**</Layout>**
</Auth0ClientProvider>
),
errorElement: <div>App Edu Error</div>,
**children: [
{
index: true,
element: <div>PageList</div>,
},
{
path: ":id",
element: <div>PageDetail</div>,
},
],**
},
];
// career-up/apps/edu/src/components/layout.styles.ts
import styled from "@emotion/styled";
export const LayoutWrapper = styled.div`
display: flex;
flex-direction: row;
gap: 24px;
margin: 0 auto;
max-width: 1128px;
padding: 16px;
.edu--layout-left {
display: flex;
flex-direction: column;
width: 225px;
gap: 10px;
}
.edu--layout-center {
display: flex;
flex-direction: column;
width: 879px;
gap: 10px;
}
`;
// career-up/apps/edu/src/atoms.ts
import { atom } from "jotai";
import { type CourseType, type UserType } from "./types";
export const userAtom = atom<UserType | null>(null);
export const coursesAtom = atom<CourseType[]>([]);
// career-up/apps/edu/src/components/layout.tsx
import React, { useEffect } from "react";
import { LayoutWrapper } from "./layout.styles";
import { useSetAtom } from "jotai";
import { coursesAtom, userAtom } from "../atoms";
import useAuth0Client from "../hooks/use-auth0-client";
import { getCourses, getUser } from "../apis";
import ProfileContainer from "../containers/profile-container";
import MyCourseInfoContainer from "../containers/my-course-info-container";
const Layout: React.FC<React.PropsWithChildren> = ({ children }) => {
const auth0Client = useAuth0Client();
const setUser = useSetAtom(userAtom);
const setCourses = useSetAtom(coursesAtom);
useEffect(() => {
(async () => {
try {
const token = await auth0Client.getTokenSilently();
getUser(token).then(setUser);
getCourses(token).then(setCourses);
} catch (error) {
alert(error);
}
})();
}, [auth0Client, setCourses, setUser]);
return (
<LayoutWrapper>
<div className="edu--layout-left">
<ProfileContainer />
<MyCourseInfoContainer />
</div>
<div className="edu--layout-center">{children}</div>
</LayoutWrapper>
);
};
export default Layout;
// career-up/apps/edu/src/components/profile.styles.ts
import styled from "@emotion/styled";
export const ProfileWrapper = styled.div`
.edu--profile-top {
display: flex;
flex-direction: column;
background-color: white;
padding: 30px 12px 16px;
border-radius: 8px 8px 0 0;
border-bottom: 1px solid rgb(0 0 0 / 0.1);
gap: 10px;
justify-content: center;
align-items: center;
img {
width: 50px;
}
.edu--profile-name {
font-size: 16px;
font-weight: bold;
}
.edu--profile-email {
color: rgb(0 0 0/0.6);
font-size: 12px;
}
}
.edu--profile-bottom {
display: flex;
flex-direction: column;
padding: 16px 12px 16px;
border-radius: 0 0 8px 8px;
background-color: white;
gap: 10px;
.edu--profile-bottom-item {
display: flex;
flex-direction: row;
justify-content: space-between;
color: rgb(0 0 0/0.6);
font-size: 14px;
.edu--profile-bottom-item-count {
color: #0a66c2;
cursor: pointer;
}
}
}
`;
// career-up/apps/edu/src/components/profile.tsx
import React from "react";
import { ProfileWrapper } from "./profile.styles";
import { type UserType } from "../types";
interface ProfileProps {
user: UserType | null;
}
const Profile: React.FC<ProfileProps> = ({ user }) => {
if (user === null) {
return null;
}
const { picture, name, email, view_count, update_count } = user;
return (
<ProfileWrapper>
<div className="edu--profile-top">
<img src={picture} />
<div className="edu--profile-name">{name}</div>
<div className="edu--profile-email">{email}</div>
</div>
<div className="edu--profile-bottom">
<div className="edu--profile-bottom-item">
<div>프로필 조회자</div>
<div className="edu--profile-bottom-item-count">{view_count}</div>
</div>
<div className="edu--profile-bottom-item">
<div>업데이트 노출</div>
<div className="edu--profile-bottom-item-count">{update_count}</div>
</div>
</div>
</ProfileWrapper>
);
};
export default Profile;
// career-up/apps/edu/src/containers/profile-container.tsx
import { useAtomValue } from "jotai";
import React from "react";
import { userAtom } from "../atoms";
import Profile from "../components/profile";
const ProfileContainer: React.FC = () => {
const user = useAtomValue(userAtom);
return <Profile user={user} />;
};
export default ProfileContainer;
// career-up/apps/edu/src/components/my-course-info.styles.ts
import styled from "@emotion/styled";
export const MyCourseInfoWrapper = styled.div`
.edu--my-course-info-top {
display: flex;
flex-direction: column;
background-color: white;
padding: 16px 12px 16px;
border-radius: 8px 8px 0 0;
border-bottom: 1px solid rgb(0 0 0 / 0.1);
gap: 10px;
.edu--my-course-info-top-title {
font-size: 15px;
font-weight: bold;
height: 15px;
}
}
.edu--my-course-info-bottom {
display: flex;
flex-direction: column;
padding: 16px 12px 16px;
border-radius: 0 0 8px 8px;
background-color: white;
gap: 10px;
.edu--my-course-info-bottom-item {
display: flex;
flex-direction: row;
justify-content: space-between;
color: rgb(0 0 0/0.6);
font-size: 14px;
.edu--my-course-info-bottom-item-count {
color: #0a66c2;
cursor: pointer;
}
}
}
`;
// career-up/apps/edu/src/components/my-course-info.tsx
import React from "react";
import { MyCourseInfoWrapper } from "./my-course-info.styles";
import { type UserType } from "../types";
interface MyCourseInfoProps {
user: UserType | null;
}
const MyCourseInfo: React.FC<MyCourseInfoProps> = ({ user }) => {
if (user === null) {
return null;
}
const { courses } = user;
return (
<MyCourseInfoWrapper>
<div className="edu--my-course-info-top">
<span className="edu--my-course-info-top-title">나의 학습 현황</span>
</div>
<div className="edu--my-course-info-bottom">
<div className="edu--my-course-info-bottom-item">
<div>전체 수강 강좌</div>
<div className="edu--my-course-info-bottom-item-count">
{courses.length}
</div>
</div>
<div className="edu--my-course-info-bottom-item">
<div>수강 중인 강좌</div>
<div className="edu--my-course-info-bottom-item-count">
{courses.filter((course) => !course.done).length}
</div>
</div>
<div className="edu--my-course-info-bottom-item">
<div>수강 완료한 강좌</div>
<div className="edu--my-course-info-bottom-item-count">
{courses.filter((course) => course.done).length}
</div>
</div>
</div>
</MyCourseInfoWrapper>
);
};
export default MyCourseInfo;
// career-up/apps/edu/src/containers/my-course-info-container.tsx
import React from "react";
import MyCourseInfo from "../components/my-course-info";
import { useAtomValue } from "jotai";
import { userAtom } from "../atoms";
const MyCourseInfoContainer: React.FC = () => {
const user = useAtomValue(userAtom);
return <MyCourseInfo user={user} />;
};
export default MyCourseInfoContainer;
// career-up/apps/edu/src/pages/page-list.tsx
import React from "react";
import CourseListItem from "../components/course-list-item";
import { coursesAtom } from "../atoms";
import { useAtomValue } from "jotai";
const PageList: React.FC = () => {
const courses = useAtomValue(coursesAtom);
return (
<>
{courses.map((course) => (
<CourseListItem key={course.id} {...course} />
))}
</>
);
};
export default PageList;