diff --git a/frontend/__tests__/e2e/components/Footer.spec.ts b/frontend/__tests__/e2e/components/Footer.spec.ts index 5641d2846b..1b73c97676 100644 --- a/frontend/__tests__/e2e/components/Footer.spec.ts +++ b/frontend/__tests__/e2e/components/Footer.spec.ts @@ -38,7 +38,7 @@ test.describe('Footer - Desktop (Chrome)', () => { }) // Mobile tests (iPhone 13) -test.use({ +test.use({ ...devices['iPhone 13'], isMobile: true, }) diff --git a/frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx b/frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx index 2104169fd0..e27ab346f9 100644 --- a/frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx +++ b/frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx @@ -10,7 +10,7 @@ import { formatDate } from 'utils/dateFormatter' import DetailsCard from 'components/CardDetailsPage' import LoadingSpinner from 'components/LoadingSpinner' import { getSimpleDuration } from 'components/ModuleCard' - + const ModuleDetailsPage = () => { const { programKey, moduleKey } = useParams<{ programKey: string; moduleKey: string }>() const [module, setModule] = useState(null) diff --git a/frontend/src/components/ActivitySection.tsx b/frontend/src/components/ActivitySection.tsx new file mode 100644 index 0000000000..6528423ef4 --- /dev/null +++ b/frontend/src/components/ActivitySection.tsx @@ -0,0 +1,56 @@ +import RecentIssues from 'components/RecentIssues' +import RecentPullRequests from 'components/RecentPullRequests' +import RecentReleases from 'components/RecentReleases' +import Milestones from 'components/Milestones' +import type { Issue } from 'types/issue' +import type { PullRequest } from 'types/pullRequest' +import type { Milestone } from 'types/milestone' +import type { Release } from 'types/release' + +interface ActivitySectionProps { + type: string + recentIssues?: Issue[] + pullRequests?: PullRequest[] + recentMilestones?: Milestone[] + recentReleases?: Release[] + showAvatar?: boolean +} + +const ActivitySection = ({ + type, + recentIssues, + pullRequests, + recentMilestones, + recentReleases, + showAvatar, +}: ActivitySectionProps) => ( + <> + {(type === 'project' || + type === 'repository' || + type === 'user' || + type === 'organization') && ( +
+ + {type === 'user' || + type === 'organization' || + type === 'repository' || + type === 'project' ? ( + + ) : ( + + )} +
+ )} + {(type === 'project' || + type === 'repository' || + type === 'organization' || + type === 'user') && ( +
+ + +
+ )} + +) + +export default ActivitySection; diff --git a/frontend/src/components/AnchorTitle.tsx b/frontend/src/components/AnchorTitle.tsx index 7557d9114e..bbcc5190e7 100644 --- a/frontend/src/components/AnchorTitle.tsx +++ b/frontend/src/components/AnchorTitle.tsx @@ -43,7 +43,11 @@ const AnchorTitle: React.FC = ({ title }) => { return (
-
+
{title}
{ const { data } = useSession() const router = useRouter() + return (
-
-
-

{title}

- {type === 'program' && accessLevel === 'admin' && canUpdateStatus && ( - - )} - {type === 'module' && - accessLevel === 'admin' && - admins?.some( - (admin) => admin.login === ((data as ExtendedSession)?.user?.login as string) - ) && ( - - )} -
- {!isActive && ( - - Inactive - - )} - {isArchived && type === 'repository' && } - {IS_PROJECT_HEALTH_ENABLED && type === 'project' && healthMetricsData.length > 0 && ( - scrollToAnchor('issues-trend')} - /> - )} -
-
-
-

{description}

- {summary && ( - }> -

{summary}

-
- )} - - {userSummary && {userSummary}} + +
- } - className={ - type === 'program' || type === 'module' - ? 'gap-2 md:col-span-7' - : type !== 'chapter' - ? 'gap-2 md:col-span-5' - : 'gap-2 md:col-span-3' - } - > - {details?.map((detail) => - detail?.label === 'Leaders' ? ( -
- {detail.label}:{' '} - -
- ) : ( -
- {detail.label}: {detail?.value || 'Unknown'} -
- ) - )} - {socialLinks && (type === 'chapter' || type === 'committee') && ( - - )} -
- {(type === 'project' || - type === 'repository' || - type === 'committee' || - type === 'user' || - type === 'organization') && ( - } - className="md:col-span-2" - > - {stats.map((stat, index) => ( -
- -
- ))} -
- )} - {type === 'chapter' && geolocationData && ( -
- -
- )} -
- {(type === 'project' || type === 'repository') && ( -
- {languages.length !== 0 && ( - } - /> - )} - {topics.length !== 0 && ( - } /> - )} -
- )} - {(type === 'program' || type === 'module') && ( -
- {tags?.length > 0 && ( - } - isDisabled={true} - /> - )} - {domains?.length > 0 && ( - } - isDisabled={true} - /> - )} -
- )} - {entityLeaders && entityLeaders.length > 0 && } - {topContributors && ( - - )} - {admins && admins.length > 0 && type === 'program' && ( - - )} - {mentors && mentors.length > 0 && ( - - )} - {(type === 'project' || - type === 'repository' || - type === 'user' || - type === 'organization') && ( -
- - {type === 'user' || - type === 'organization' || - type === 'repository' || - type === 'project' ? ( - - ) : ( - - )} -
- )} - {(type === 'project' || - type === 'repository' || - type === 'organization' || - type === 'user') && ( -
- - -
- )} - {(type === 'project' || type === 'user' || type === 'organization') && - repositories.length > 0 && ( - }> - - - )} - {type === 'program' && modules.length > 0 && ( - } - > - - - )} - {IS_PROJECT_HEALTH_ENABLED && type === 'project' && healthMetricsData.length > 0 && ( - - )} - {entityKey && ['chapter', 'project', 'repository'].includes(type) && ( - - )} + +
+ + + + + + +
) } -export default DetailsCard - -export const SocialLinks = ({ urls }) => { - if (!urls || urls.length === 0) return null - return ( -
- Social Links - -
- ) -} +export default DetailsCard; \ No newline at end of file diff --git a/frontend/src/components/ContributorsSection.tsx b/frontend/src/components/ContributorsSection.tsx new file mode 100644 index 0000000000..2ba78a66bc --- /dev/null +++ b/frontend/src/components/ContributorsSection.tsx @@ -0,0 +1,41 @@ +import { faUsers } from '@fortawesome/free-solid-svg-icons' +import TopContributorsList from 'components/TopContributorsList' +import type { Contributor } from 'types/contributor' + +interface ContributorsSectionProps { + topContributors?: Contributor[] + mentors?: Contributor[] + admins?: Contributor[] + type: string +} + +const ContributorsSection = ({ + topContributors, + admins, + mentors, + type, +}: ContributorsSectionProps ) => ( + <> + {topContributors && ( + + )} + {admins && admins.length > 0 && type === 'program' && ( + + )} + {mentors && mentors.length > 0 && ( + + )} + +) + +export default ContributorsSection; diff --git a/frontend/src/components/DetailsSection.tsx b/frontend/src/components/DetailsSection.tsx new file mode 100644 index 0000000000..8d2b6f796d --- /dev/null +++ b/frontend/src/components/DetailsSection.tsx @@ -0,0 +1,65 @@ +import { faRectangleList } from '@fortawesome/free-solid-svg-icons' +import upperFirst from 'lodash/upperFirst' +import SecondaryCard from 'components/SecondaryCard' +import AnchorTitle from 'components/AnchorTitle' +import LeadersList from 'components/LeadersList' +import ChapterMapWrapper from 'components/ChapterMapWrapper' +import SocialLinks from 'components/SocialLinks' +import type { JSX } from 'react' +import type { Chapter } from 'types/chapter' + +interface DetailsSectionProps { + details?: { label: string; value: string | JSX.Element }[] + socialLinks?: string[] + type: string + geolocationData?: Chapter[] +} + +const DetailsSection = ({ details, socialLinks, type, geolocationData }: DetailsSectionProps) => ( + <> + } + className={ + type === 'program' || type === 'module' + ? 'gap-2 md:col-span-7' + : type !== 'chapter' + ? 'gap-2 md:col-span-5' + : 'gap-2 md:col-span-3' + } + > + {details?.map((detail) => + detail?.label === 'Leaders' ? ( +
+ {detail.label}:{' '} + +
+ ) : ( +
+ {detail.label}: {detail?.value || 'Unknown'} +
+ ) + )} + {socialLinks && (type === 'chapter' || type === 'committee') && ( + + )} +
+ {type === 'chapter' && geolocationData && ( +
+ +
+ )} + +) + +export default DetailsSection diff --git a/frontend/src/components/HeaderSection.tsx b/frontend/src/components/HeaderSection.tsx new file mode 100644 index 0000000000..2c9c25a894 --- /dev/null +++ b/frontend/src/components/HeaderSection.tsx @@ -0,0 +1,76 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCircleInfo } from '@fortawesome/free-solid-svg-icons' +import { useRouter } from 'next/navigation' +import { IS_PROJECT_HEALTH_ENABLED } from 'utils/env.client' +import { scrollToAnchor } from 'utils/scrollToAnchor' +import ProgramActions from 'components/ProgramActions' +import MetricsScoreCircle from 'components/MetricsScoreCircle' +import { Contributor } from 'types/contributor' +import { HealthMetricsProps } from 'types/healthMetrics' + +interface HeaderSectionProps { + title?: string + type: string + accessLevel?: string + status: string + setStatus: (status: string) => void + canUpdateStatus?: boolean + admins?: Contributor[] + userLogin?: string + router: ReturnType + isActive?: boolean + healthMetricsData: HealthMetricsProps[] + description?: string +} + +const HeaderSection = ({ + title, + type, + accessLevel, + status, + setStatus, + canUpdateStatus, + admins, + userLogin, + router, + isActive, + healthMetricsData, + description +}: HeaderSectionProps) => ( + <> +
+
+

{title}

+ {type === 'program' && accessLevel === 'admin' && canUpdateStatus && ( + + )} + {type === 'module' && + accessLevel === 'admin' && + admins?.some((admin) => admin.login === userLogin) && ( + + )} + {IS_PROJECT_HEALTH_ENABLED && type === 'project' && healthMetricsData.length > 0 && ( + scrollToAnchor('issues-trend')} + /> + )} +
+ {!isActive && ( + + Inactive + + )} +
+ {description &&

{description}

} + +) + +export default HeaderSection diff --git a/frontend/src/components/HealthSection.tsx b/frontend/src/components/HealthSection.tsx new file mode 100644 index 0000000000..a420750ecb --- /dev/null +++ b/frontend/src/components/HealthSection.tsx @@ -0,0 +1,15 @@ +import { IS_PROJECT_HEALTH_ENABLED } from 'utils/env.client' +import HealthMetrics from 'components/HealthMetrics' +import type { HealthMetricsProps } from 'types/healthMetrics' + +interface HealthSectionProps { + healthMetricsData?: HealthMetricsProps[] + type: string +} + +const HealthSection = ({ healthMetricsData, type }: HealthSectionProps) => + IS_PROJECT_HEALTH_ENABLED && + type === 'project' && + healthMetricsData?.length > 0 && + +export default HealthSection; diff --git a/frontend/src/components/ListsSection.tsx b/frontend/src/components/ListsSection.tsx new file mode 100644 index 0000000000..8194ef36e5 --- /dev/null +++ b/frontend/src/components/ListsSection.tsx @@ -0,0 +1,56 @@ +import { faCode, faTags, faChartPie } from '@fortawesome/free-solid-svg-icons' +import ToggleableList from 'components/ToggleableList' +import AnchorTitle from 'components/AnchorTitle' + +interface ListsSectionProps { + languages?: string[] + topics?: string[] + tags?: string[] + domains?: string[] + type: string +} + +const ListsSection = ({ languages, topics, tags, domains, type }: ListsSectionProps) => ( + <> + {(type === 'project' || type === 'repository') && ( +
+ {languages.length !== 0 && ( + } + /> + )} + {topics.length !== 0 && ( + } /> + )} +
+ )} + {(type === 'program' || type === 'module') && ( +
+ {tags?.length > 0 && ( + } + isDisabled={true} + /> + )} + {domains?.length > 0 && ( + } + isDisabled={true} + /> + )} +
+ )} + +) + +export default ListsSection; diff --git a/frontend/src/components/MetricsSection.tsx b/frontend/src/components/MetricsSection.tsx new file mode 100644 index 0000000000..10027e281d --- /dev/null +++ b/frontend/src/components/MetricsSection.tsx @@ -0,0 +1,45 @@ +import { faChartPie } from '@fortawesome/free-solid-svg-icons' +import SecondaryCard from 'components/SecondaryCard' +import AnchorTitle from 'components/AnchorTitle' +import InfoBlock from 'components/InfoBlock' +import type { IconDefinition } from '@fortawesome/free-solid-svg-icons' + +type Stats = { + icon: IconDefinition + pluralizedName?: string + unit?: string + value: number +} + + +interface MetricsSectionProps { + stats?: Stats[] + type: string +} + +const MetricsSection = ({ stats, type }: MetricsSectionProps) => + (type === 'project' || + type === 'repository' || + type === 'committee' || + type === 'user' || + type === 'organization') && ( + } + className="md:col-span-2" + > + {stats.map((stat, index) => ( +
+ +
+ ))} +
+ ) + +export default MetricsSection; diff --git a/frontend/src/components/ModulesSection.tsx b/frontend/src/components/ModulesSection.tsx new file mode 100644 index 0000000000..8f5d62a214 --- /dev/null +++ b/frontend/src/components/ModulesSection.tsx @@ -0,0 +1,26 @@ +import { faFolderOpen } from '@fortawesome/free-solid-svg-icons' +import SecondaryCard from 'components/SecondaryCard' +import AnchorTitle from 'components/AnchorTitle' +import ModuleCard from 'components/ModuleCard' +import { Contributor } from 'types/contributor' +import type { Module } from 'types/mentorship' + +interface ModulesSectionProps { + modules?: Module[] + accessLevel: string + admins?: Contributor[] + type: string +} + +const ModulesSection = ({ modules, accessLevel, admins, type }: ModulesSectionProps) => + type === 'program' && + modules.length > 0 && ( + } + > + + + ) + +export default ModulesSection; diff --git a/frontend/src/components/RepositoriesSection.tsx b/frontend/src/components/RepositoriesSection.tsx new file mode 100644 index 0000000000..89a1906ae3 --- /dev/null +++ b/frontend/src/components/RepositoriesSection.tsx @@ -0,0 +1,20 @@ +import { faFolderOpen } from '@fortawesome/free-solid-svg-icons' +import SecondaryCard from 'components/SecondaryCard' +import AnchorTitle from 'components/AnchorTitle' +import RepositoriesCard from 'components/RepositoriesCard' +import type { RepositoryCardProps } from 'types/project' + +interface RepositoriesSectionProps { + repositories?: RepositoryCardProps[] + type: string +} + +const RepositoriesSection = ({ repositories, type }: RepositoriesSectionProps) => + (type === 'project' || type === 'user' || type === 'organization') && + repositories.length > 0 && ( + }> + + + ) + +export default RepositoriesSection; diff --git a/frontend/src/components/SocialLinks.tsx b/frontend/src/components/SocialLinks.tsx new file mode 100644 index 0000000000..1fe82b8736 --- /dev/null +++ b/frontend/src/components/SocialLinks.tsx @@ -0,0 +1,30 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { getSocialIcon } from 'utils/urlIconMappings' + +interface SocialLinksProps { + urls: string[] +} + +const SocialLinks = ({ urls }: SocialLinksProps) => { + if (!urls || urls.length === 0) return null + return ( +
+ Social Links +
+ {urls.map((url, index) => ( + + + + ))} +
+
+ ) +} + +export default SocialLinks diff --git a/frontend/src/components/SponsorsSection.tsx b/frontend/src/components/SponsorsSection.tsx new file mode 100644 index 0000000000..43a0d3f435 --- /dev/null +++ b/frontend/src/components/SponsorsSection.tsx @@ -0,0 +1,20 @@ +import SponsorCard from 'components/SponsorCard' + +interface SponsorsSectionProps { + entityKey?: string + projectName?: string + title?: string + type: string +} + +const SponsorsSection = ({ entityKey, projectName, title, type }: SponsorsSectionProps) => + entityKey && + ['chapter', 'project', 'repository'].includes(type) && ( + + ) + +export default SponsorsSection; diff --git a/frontend/src/components/SummarySection.tsx b/frontend/src/components/SummarySection.tsx new file mode 100644 index 0000000000..10a4a78cd8 --- /dev/null +++ b/frontend/src/components/SummarySection.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import { faCircleInfo } from '@fortawesome/free-solid-svg-icons' +import AnchorTitle from 'components/AnchorTitle' +import SecondaryCard from 'components/SecondaryCard' +import type { JSX } from 'react' + +interface SummarySectionProps { + summary?: string | null + userSummary?: JSX.Element +} + +const SummarySection: React.FC = ({ summary, userSummary }) => { + if (!summary && !userSummary) return null + + return ( +
+ {summary && ( + }> +

{summary}

+
+ )} + + {userSummary && {userSummary}} +
+ ) +} + +export default SummarySection; diff --git a/frontend/src/types/components.ts b/frontend/src/types/components.ts new file mode 100644 index 0000000000..6ffecb9593 --- /dev/null +++ b/frontend/src/types/components.ts @@ -0,0 +1,97 @@ +import { IconDefinition } from '@fortawesome/free-solid-svg-icons' +import type { ReactElement } from 'react' + +export interface HeaderSectionProps { + title: string + type: string + accessLevel: string + status: string + setStatus: (status: string) => void + canUpdateStatus: boolean + admins?: Array<{ login: string }> + description: string + userLogin?: string + router: any + isActive: boolean + healthMetricsData: Array<{ score: number }> +} + +export interface SummarySectionProps { + summary?: string + userSummary?: ReactElement +} + +export interface DetailsSectionProps { + details: Array<{ label: string; value: string | number }> + socialLinks?: string[] + type: string + geolocationData: { lat: number; lng: number } | null +} + +export interface MetricsSectionProps { + stats: Array<{ + icon: IconDefinition + pluralizedName: string + unit: string + value: number + }> + type: string +} + +export interface ListsSectionProps { + languages?: string[] + topics?: string[] + tags?: string[] + domains?: string[] + type: string +} + +export interface Contributor { + login: string + contributions?: number + avatarUrl: string + name: string +} + +export interface ContributorsSectionProps { + topContributors: Contributor[] | null + admins?: Contributor[] + mentors?: Contributor[] + type: string +} + +export interface Issue { + title: string + number: number + url: string + createdAt: number + updatedAt?: number +} + +export interface PullRequest { + title: string + number: number + url: string + createdAt: number +} + +export interface Milestone { + title: string + description: string + dueDate?: number +} + +export interface Release { + title: string + description: string + publishedAt: number +} + +export interface ActivitySectionProps { + type: string + recentIssues: Issue[] + pullRequests: PullRequest[] + recentMilestones: Milestone[] + recentReleases: Release[] + showAvatar: boolean +}