diff --git a/src/components/Dashboard/DashboardPage.tsx b/src/components/Dashboard/DashboardPage.tsx index b7d832983..aa7914a75 100644 --- a/src/components/Dashboard/DashboardPage.tsx +++ b/src/components/Dashboard/DashboardPage.tsx @@ -4,11 +4,12 @@ import { useToast } from '../../hooks/use-toast'; import { useStore } from '@nanostores/react'; import { $teamList } from '../../stores/team'; import type { TeamListResponse } from '../TeamDropdown/TeamDropdown'; -import { DashboardTab } from './DashboardTab'; +import { DashboardTabButton } from './DashboardTabButton'; import { PersonalDashboard, type BuiltInRoadmap } from './PersonalDashboard'; import { TeamDashboard } from './TeamDashboard'; import { getUser } from '../../lib/jwt'; import { useParams } from '../../hooks/use-params'; +import { cn } from '../../../editor/utils/classname'; type DashboardPageProps = { builtInRoleRoadmaps?: BuiltInRoadmap[]; @@ -66,23 +67,20 @@ export function DashboardPage(props: DashboardPageProps) { : '/images/default-avatar.png'; return ( -
-
-
- +
+
+ - {isLoading && ( - <> - - - - )} - {!isLoading && ( <> {teamList.map((team) => { @@ -91,7 +89,7 @@ export function DashboardPage(props: DashboardPageProps) { ? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}` : '/images/default-avatar.png'; return ( - ); })} - )}
+
+
{!selectedTeamId && !isTeamPage && ( - +
+ +
)} {(selectedTeamId || isTeamPage) && ( - +
+ +
)}
-
+ ); } diff --git a/src/components/Dashboard/DashboardTab.tsx b/src/components/Dashboard/DashboardTabButton.tsx similarity index 66% rename from src/components/Dashboard/DashboardTab.tsx rename to src/components/Dashboard/DashboardTabButton.tsx index c6b345599..2daa41c84 100644 --- a/src/components/Dashboard/DashboardTab.tsx +++ b/src/components/Dashboard/DashboardTabButton.tsx @@ -11,7 +11,7 @@ type DashboardTabProps = { icon?: ReactNode; }; -export function DashboardTab(props: DashboardTabProps) { +export function DashboardTabButton(props: DashboardTabProps) { const { isActive, onClick, label, className, href, avatar, icon } = props; const Slot = href ? 'a' : 'button'; @@ -20,8 +20,10 @@ export function DashboardTab(props: DashboardTabProps) { )} {icon} diff --git a/src/components/Dashboard/PersonalDashboard.tsx b/src/components/Dashboard/PersonalDashboard.tsx index eb23d7c98..f61f5115c 100644 --- a/src/components/Dashboard/PersonalDashboard.tsx +++ b/src/components/Dashboard/PersonalDashboard.tsx @@ -18,6 +18,10 @@ import type { AllowedProfileVisibility } from '../../api/user.ts'; import { PencilIcon, type LucideIcon } from 'lucide-react'; import { cn } from '../../lib/classname.ts'; import type { AllowedRoadmapRenderer } from '../../lib/roadmap.ts'; +import { + FavoriteRoadmaps, + type AIRoadmapType, +} from '../HeroSection/FavoriteRoadmaps.tsx'; type UserDashboardResponse = { name: string; @@ -28,11 +32,7 @@ type UserDashboardResponse = { profileVisibility: AllowedProfileVisibility; progresses: UserProgress[]; projects: ProjectStatusDocument[]; - aiRoadmaps: { - id: string; - title: string; - slug: string; - }[]; + aiRoadmaps: AIRoadmapType[]; topicDoneToday: number; }; @@ -138,7 +138,9 @@ export function PersonalDashboard(props: PersonalDashboardProps) { return () => window.removeEventListener('refresh-favorites', loadProgress); }, []); - const learningRoadmapsToShow = (personalDashboardDetails?.progresses || []) + const learningRoadmapsToShow: UserProgress[] = ( + personalDashboardDetails?.progresses || [] + ) .filter((progress) => !progress.isCustomResource) .sort((a, b) => { const updatedAtA = new Date(a.updatedAt); @@ -156,7 +158,10 @@ export function PersonalDashboard(props: PersonalDashboardProps) { }); const aiGeneratedRoadmaps = personalDashboardDetails?.aiRoadmaps || []; - const customRoadmaps = (personalDashboardDetails?.progresses || []) + + const customRoadmaps: UserProgress[] = ( + personalDashboardDetails?.progresses || [] + ) .filter((progress) => progress.isCustomResource) .sort((a, b) => { const updatedAtA = new Date(a.updatedAt); @@ -231,8 +236,23 @@ export function PersonalDashboard(props: PersonalDashboardProps) { const { username } = personalDashboardDetails || {}; + // later on it will be switching of version based on localstorage + if (true) { + return ( +
+ +
+ ); + } + return ( -
+
{isLoading ? (
) : ( diff --git a/src/components/HeroSection/FavoriteRoadmaps.tsx b/src/components/HeroSection/FavoriteRoadmaps.tsx index a639be626..1440e0ff8 100644 --- a/src/components/HeroSection/FavoriteRoadmaps.tsx +++ b/src/components/HeroSection/FavoriteRoadmaps.tsx @@ -1,161 +1,274 @@ -import { useEffect, useState } from 'react'; -import { EmptyProgress } from './EmptyProgress'; -import { httpGet } from '../../lib/http'; -import { HeroRoadmaps, type HeroTeamRoadmaps } from './HeroRoadmaps'; -import { isLoggedIn } from '../../lib/jwt'; -import type { AllowedMemberRoles } from '../ShareOptions/ShareTeamMemberList.tsx'; +import { FolderKanban, MapIcon, Plus, Sparkle, ThumbsUp } from 'lucide-react'; +import type { ReactNode } from 'react'; +import type { ResourceType } from '../../lib/resource-progress.ts'; +import { CreateRoadmapButton } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapButton.tsx'; +import { MarkFavorite } from '../FeaturedItems/MarkFavorite.tsx'; +import { CheckIcon } from '../ReactIcons/CheckIcon.tsx'; +import { Spinner } from '../ReactIcons/Spinner.tsx'; +import type { UserProgress } from '../TeamProgress/TeamProgressPage.tsx'; +import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions.tsx'; +import { getRelativeTimeString } from '../../lib/date'; + +export type AIRoadmapType = { + id: string; + title: string; + slug: string; +}; + +type ProgressRoadmapProps = { + url: string; + percentageDone: number; + allowFavorite?: boolean; -export type UserProgressResponse = { resourceId: string; - resourceType: 'roadmap' | 'best-practice'; + resourceType: ResourceType; resourceTitle: string; - isFavorite: boolean; - done: number; - learning: number; - skipped: number; - total: number; - updatedAt: Date; - isCustomResource: boolean; - roadmapSlug?: string; - team?: { - name: string; - id: string; - role: AllowedMemberRoles; - }; -}[]; + isFavorite?: boolean; +}; -function renderProgress(progressList: UserProgressResponse) { - progressList.forEach((progress) => { - const href = - progress.resourceType === 'best-practice' - ? `/best-practices/${progress.resourceId}` - : `/${progress.resourceId}`; - const element = document.querySelector(`a[href="${href}"]`); - if (!element) { - return; - } - - window.dispatchEvent( - new CustomEvent('mark-favorite', { - detail: { - resourceId: progress.resourceId, - resourceType: progress.resourceType, - isFavorite: progress.isFavorite, - }, - }), - ); - - const totalDone = progress.done + progress.skipped; - const percentageDone = (totalDone / progress.total) * 100; - - const progressBar: HTMLElement | null = - element.querySelector('[data-progress]'); - if (progressBar) { - progressBar.style.width = `${percentageDone}%`; - } - }); -} - -type ProgressResponse = UserProgressResponse; - -export function FavoriteRoadmaps() { - const isAuthenticated = isLoggedIn(); - if (!isAuthenticated) { - return null; - } - - const [isPreparing, setIsPreparing] = useState(true); - const [isLoading, setIsLoading] = useState(true); - const [progress, setProgress] = useState([]); - const [containerOpacity, setContainerOpacity] = useState(0); - - function showProgressContainer() { - const heroEl = document.getElementById('hero-text')!; - if (!heroEl) { - return; - } - - heroEl.classList.add('opacity-0'); - setTimeout(() => { - heroEl.parentElement?.removeChild(heroEl); - setIsPreparing(false); - - setTimeout(() => { - setContainerOpacity(100); - }, 50); - }, 0); - } - - async function loadProgress() { - setIsLoading(true); - - const { response: progressList, error } = await httpGet( - `${import.meta.env.PUBLIC_API_URL}/v1-get-hero-roadmaps`, - ); - - if (error || !progressList) { - return; - } - - setProgress(progressList); - setIsLoading(false); - showProgressContainer(); - - // render progress on featured items - renderProgress(progressList); - } - - useEffect(() => { - loadProgress().finally(() => { - setIsLoading(false); - }); - }, []); - - useEffect(() => { - window.addEventListener('refresh-favorites', loadProgress); - return () => window.removeEventListener('refresh-favorites', loadProgress); - }, []); - - if (isPreparing) { - return null; - } - - const hasProgress = progress?.length > 0; - const customRoadmaps = progress?.filter( - (p) => p.isCustomResource && !p.team?.name, - ); - const defaultRoadmaps = progress?.filter((p) => !p.isCustomResource); - const teamRoadmaps: HeroTeamRoadmaps = progress - ?.filter((p) => p.isCustomResource && p.team?.name) - .reduce((acc: HeroTeamRoadmaps, curr) => { - const currTeam = curr.team!; - if (!acc[currTeam.name]) { - acc[currTeam.name] = []; - } - - acc[currTeam.name].push(curr); - - return acc; - }, {}); +export function HeroRoadmap(props: ProgressRoadmapProps) { + const { + url, + percentageDone, + resourceType, + resourceId, + resourceTitle, + isFavorite, + allowFavorite = true, + } = props; return ( -
-
-
- {!isLoading && progress?.length == 0 && } - {hasProgress && ( - + + {resourceTitle} + + + + + {allowFavorite && ( + + )} + + ); +} + +type HeroTitleProps = { + icon: any; + isLoading?: boolean; + title: string | ReactNode; +}; + +function HeroTitle(props: HeroTitleProps) { + const { isLoading = false, title, icon } = props; + + return ( +

+ {!isLoading && icon} + {isLoading && ( + + + + )} + {title} +

+ ); +} + +type FavoriteRoadmapsProps = { + progress: UserProgress[]; + projects: (ProjectStatusDocument & { + title: string; + })[]; + customRoadmaps: UserProgress[]; + aiRoadmaps: AIRoadmapType[]; + isLoading: boolean; +}; + +type HeroProjectProps = { + project: ProjectStatusDocument & { + title: string; + }; +}; + +export function HeroProject({ project }: HeroProjectProps) { + return ( + +
+

+ {project.title} +

+ + {project.submittedAt && project.repositoryUrl + ? 'Submitted' + : 'In Progress'} + +
+
+ + + {project.upvotes} + + {project.startedAt && ( + Started {getRelativeTimeString(project.startedAt)} + )} +
+ +
+ {project.submittedAt && project.repositoryUrl && ( +
+ )} + + ); +} + +export function FavoriteRoadmaps(props: FavoriteRoadmapsProps) { + const { progress, isLoading, customRoadmaps, aiRoadmaps, projects } = props; + + return ( +
+
+
+ + ) as any + } + isLoading={isLoading} + title="Your progress and bookmarks" + /> + {!isLoading && progress.length > 0 && ( +
+ {progress.map((resource) => ( + + ))} + +
+ )} +
+
+ +
+
+ ) as any} + isLoading={isLoading} + title="Your custom roadmaps" + /> + {!isLoading && customRoadmaps.length > 0 && ( +
+ {customRoadmaps.map((customRoadmap) => ( + + ))} + +
+ )} +
+
+ +
+
+ ) as any} + isLoading={isLoading} + title="Your AI roadmaps" + /> + {!isLoading && aiRoadmaps.length > 0 && ( +
+ {aiRoadmaps.map((aiRoadmap) => ( + + ))} + + + + Generate New + +
+ )} +
+
+ +
+
+ ) as any + } + isLoading={isLoading} + title="Your projects" + /> + {!isLoading && projects.length > 0 && ( +
+ {projects.map((project) => ( + + ))} + + + + Start a new project + +
)}
diff --git a/src/components/HeroSection/HeroRoadmaps.tsx b/src/components/HeroSection/HeroRoadmaps.tsx deleted file mode 100644 index 7633f0ab7..000000000 --- a/src/components/HeroSection/HeroRoadmaps.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import type { UserProgressResponse } from './FavoriteRoadmaps'; -import { CheckIcon } from '../ReactIcons/CheckIcon'; -import { MarkFavorite } from '../FeaturedItems/MarkFavorite'; -import { Spinner } from '../ReactIcons/Spinner'; -import type { ResourceType } from '../../lib/resource-progress'; -import { MapIcon, Users2 } from 'lucide-react'; -import { CreateRoadmapButton } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapButton'; -import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal'; -import { type ReactNode, useState } from 'react'; -import { FeatureAnnouncement } from '../FeatureAnnouncement.tsx'; - -type ProgressRoadmapProps = { - url: string; - percentageDone: number; - allowFavorite?: boolean; - - resourceId: string; - resourceType: ResourceType; - resourceTitle: string; - isFavorite?: boolean; -}; -function HeroRoadmap(props: ProgressRoadmapProps) { - const { - url, - percentageDone, - resourceType, - resourceId, - resourceTitle, - isFavorite, - allowFavorite = true, - } = props; - - return ( - - {resourceTitle} - - - - {allowFavorite && ( - - )} - - ); -} - -type ProgressTitleProps = { - icon: any; - isLoading?: boolean; - title: string | ReactNode; -}; - -export function HeroTitle(props: ProgressTitleProps) { - const { isLoading = false, title, icon } = props; - - return ( -

- {!isLoading && icon} - {isLoading && ( - - - - )} - {title} -

- ); -} -export type HeroTeamRoadmaps = Record; - -type ProgressListProps = { - progress: UserProgressResponse; - customRoadmaps: UserProgressResponse; - teamRoadmaps?: HeroTeamRoadmaps; - isLoading?: boolean; -}; - -export function HeroRoadmaps(props: ProgressListProps) { - const { - teamRoadmaps = {}, - progress, - isLoading = false, - customRoadmaps, - } = props; - - const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false); - const [creatingRoadmapTeamId, setCreatingRoadmapTeamId] = useState(); - - return ( -
-

- -

- {isCreatingRoadmap && ( - { - setIsCreatingRoadmap(false); - setCreatingRoadmapTeamId(undefined); - }} - /> - )} - { - ) as any - } - isLoading={isLoading} - title="Your progress and favorite roadmaps." - /> - } - -
- {progress.map((resource) => ( - - ))} -
- -
- { - } - title="Your custom roadmaps" - /> - } - - {customRoadmaps.length === 0 && ( -

- You haven't created any custom roadmaps yet.{' '} - -

- )} - - {customRoadmaps.length > 0 && ( -
- {customRoadmaps.map((customRoadmap) => { - return ( - - ); - })} - - -
- )} -
- - {Object.keys(teamRoadmaps).map((teamName) => { - const currentTeam: UserProgressResponse[0]['team'] = - teamRoadmaps?.[teamName]?.[0]?.team; - const roadmapsList = teamRoadmaps[teamName].filter( - (roadmap) => !!roadmap.resourceTitle, - ); - const canManageTeam = ['admin', 'manager'].includes(currentTeam?.role!); - - return ( -
- { - } - title={ - <> - Team{' '} - - {teamName} - - Roadmaps - - } - /> - } - - {roadmapsList.length === 0 && ( -

- Team does not have any roadmaps yet.{' '} - {canManageTeam && ( - - )} -

- )} - - {roadmapsList.length > 0 && ( -
- {roadmapsList.map((customRoadmap) => { - return ( - - ); - })} - - {canManageTeam && ( - - )} -
- )} -
- ); - })} -
- ); -} diff --git a/src/components/RoadCard/RoadmapSelect.tsx b/src/components/RoadCard/RoadmapSelect.tsx index 2db7bc2e4..baa7fae22 100644 --- a/src/components/RoadCard/RoadmapSelect.tsx +++ b/src/components/RoadCard/RoadmapSelect.tsx @@ -1,8 +1,8 @@ import { httpGet } from '../../lib/http'; import { useEffect, useState } from 'react'; import { pageProgressMessage } from '../../stores/page'; -import type { UserProgressResponse } from '../HeroSection/FavoriteRoadmaps'; import { SelectionButton } from './SelectionButton'; +import type { UserProgressResponse } from '../Roadmaps/RoadmapsPage'; type RoadmapSelectProps = { selectedRoadmaps: string[]; diff --git a/src/components/Roadmaps/RoadmapsPage.tsx b/src/components/Roadmaps/RoadmapsPage.tsx index defe3e3e8..91b6a72e1 100644 --- a/src/components/Roadmaps/RoadmapsPage.tsx +++ b/src/components/Roadmaps/RoadmapsPage.tsx @@ -10,8 +10,27 @@ import { } from '../../lib/browser.ts'; import { RoadmapCard } from './RoadmapCard.tsx'; import { httpGet } from '../../lib/http.ts'; -import type { UserProgressResponse } from '../HeroSection/FavoriteRoadmaps.tsx'; import { isLoggedIn } from '../../lib/jwt.ts'; +import type { AllowedMemberRoles } from '../ShareOptions/ShareTeamMemberList.tsx'; + +export type UserProgressResponse = { + resourceId: string; + resourceType: 'roadmap' | 'best-practice'; + resourceTitle: string; + isFavorite: boolean; + done: number; + learning: number; + skipped: number; + total: number; + updatedAt: Date; + isCustomResource: boolean; + roadmapSlug?: string; + team?: { + name: string; + id: string; + role: AllowedMemberRoles; + }; +}[]; const groupNames = [ 'Absolute Beginners', diff --git a/src/components/TeamProgress/TeamProgressPage.tsx b/src/components/TeamProgress/TeamProgressPage.tsx index 78e8c7585..74784f959 100644 --- a/src/components/TeamProgress/TeamProgressPage.tsx +++ b/src/components/TeamProgress/TeamProgressPage.tsx @@ -1,16 +1,15 @@ +import { useStore } from '@nanostores/react'; import { useEffect, useState } from 'react'; +import { useAuth } from '../../hooks/use-auth'; +import { useToast } from '../../hooks/use-toast'; +import { getUrlParams, setUrlParams } from '../../lib/browser'; import { httpGet } from '../../lib/http'; import { pageProgressMessage } from '../../stores/page'; -import { MemberProgressItem } from './MemberProgressItem'; -import { useToast } from '../../hooks/use-toast'; -import { useStore } from '@nanostores/react'; import { $currentTeam } from '../../stores/team'; import { GroupRoadmapItem } from './GroupRoadmapItem'; -import { getUrlParams, setUrlParams } from '../../lib/browser'; -import { useAuth } from '../../hooks/use-auth'; -import { MemberProgressModal } from './MemberProgressModal'; import { MemberCustomProgressModal } from './MemberCustomProgressModal'; -import { canManageCurrentRoadmap } from '../../stores/roadmap.ts'; +import { MemberProgressItem } from './MemberProgressItem'; +import { MemberProgressModal } from './MemberProgressModal'; export type UserProgress = { resourceTitle: string; diff --git a/src/pages/dashboard.astro b/src/pages/dashboard.astro index 50d8afcce..ec1d3d81c 100644 --- a/src/pages/dashboard.astro +++ b/src/pages/dashboard.astro @@ -56,7 +56,7 @@ const enrichedBestPractices = bestPractices.map((bestPractice) => { }); --- - + { client:load />
-
+
diff --git a/src/styles/global.css b/src/styles/global.css index ab2e901cb..c3738d341 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -128,8 +128,20 @@ a > code:before { animation: barberpole 15s linear infinite; } +.striped-loader-slate { + background-image: repeating-linear-gradient( + -45deg, + transparent, + transparent 5px, + hsla(0, 0%, 0%, 0.1) 5px, + hsla(0, 0%, 0%, 0.1) 10px + ); + background-size: 200% 200%; + animation: barberpole 30s linear infinite; +} + @keyframes barberpole { 100% { background-position: 100% 100%; } -} \ No newline at end of file +}