Skip to content
Closed
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 @@ -23,7 +23,7 @@ import { ClearManageFiltersButton } from '@/components/shared/ClearManageFilters
import { DocsLink } from '@/components/shared/DocsLink'
import { EntityGrid } from '@/components/shared/EntityGrid'
import { FilterToggle } from '@/components/shared/FilterToggle'
import { type ChallengeMeta, ProjectCard } from '@/components/shared/ProjectCard'
import { ProjectCard } from '@/components/shared/ProjectCard'
import { SearchBar } from '@/components/shared/SearchBar'
import { ViewModeToggle } from '@/components/shared/ViewModeToggle'
import {
Expand Down Expand Up @@ -87,6 +87,7 @@ export const ManageProjectsContent = () => {
pinnedProjectIds,
toggleProjectPin,
challengeCountsByProjectId,
challengeMetaByProjectId,
updateProject,
handleExportCsv,
handleArchiveProject,
Expand All @@ -109,6 +110,36 @@ export const ManageProjectsContent = () => {
const projectId = proj.id
return (
<div className="flex items-center gap-1">
{projectId != null && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={(e) => {
e.preventDefault()
handleArchiveProject(projectId, proj.isArchived ?? false)
}}
title={
proj.isArchived
? t('common.unarchiveProject', undefined, 'Unarchive project')
: t('common.archiveProject', undefined, 'Archive project')
}
aria-label={
proj.isArchived
? t('common.unarchiveProject', undefined, 'Unarchive project')
: t('common.archiveProject', undefined, 'Archive project')
}
>
<Archive
className={cn(
'h-4 w-4',
proj.isArchived
? 'text-amber-600 dark:text-amber-400'
: 'text-zinc-400 hover:text-zinc-600 dark:text-zinc-500 dark:hover:text-zinc-400'
)}
/>
</Button>
)}
{projectId != null && (
<Button
variant="ghost"
Expand Down Expand Up @@ -465,13 +496,8 @@ export const ManageProjectsContent = () => {
renderItem={(proj) => {
const projectId = proj.id
const isPinned = projectId != null && pinnedProjectIds.includes(projectId)
const count =
projectId != null ? (challengeCountsByProjectId[projectId] ?? 0) : 0
const challengeMeta: ChallengeMeta = {
totalChallenges: count,
pinned: 0,
completed: 0,
}
const challengeMeta =
projectId != null ? challengeMetaByProjectId[projectId] : undefined
return (
<ProjectCard
project={proj}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@ import { useQuery } from '@tanstack/react-query'
import type { ReactNode } from 'react'
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { api } from '@/api'
import type { ChallengeMeta } from '@/components/shared/ProjectCard'
import { useAuthContext } from '@/contexts/AuthContext'
import { CHALLENGE_STATUS_FINISHED } from '@/lib/challengeStatus'
import type { Project } from '@/types/Project'
import { buildPropertiesWithPinnedProjects, getPinnedProjectIds } from './pinnedProjects'
import {
buildPropertiesWithPinnedProjects,
getPinnedChallengeIds,
getPinnedProjectIds,
} from './pinnedProjects'

type ViewMode = 'grid' | 'list'

Expand Down Expand Up @@ -45,6 +51,7 @@ interface ManageProjectsContextType {

// Challenge counts
challengeCountsByProjectId: Record<number, number>
challengeMetaByProjectId: Record<number, ChallengeMeta>

// Mutations
updateProject: (projectId: number, updates: Partial<Project>) => void
Expand Down Expand Up @@ -98,6 +105,7 @@ export const ManageProjectsProvider = ({ children }: { children: ReactNode }) =>
const updateProjectMutation = api.project.useUpdateProject()
const deleteProjectMutation = api.project.useDeleteProject()
const pinnedProjectIds = useMemo(() => getPinnedProjectIds(user), [user])
const pinnedChallengeIds = useMemo(() => getPinnedChallengeIds(user), [user])

const projectsSortedByPinned = useMemo(() => {
if (!projects?.length) return projects ?? []
Expand All @@ -111,7 +119,7 @@ export const ManageProjectsProvider = ({ children }: { children: ReactNode }) =>

const projectsToShow = useMemo(() => {
const list = projectsSortedByPinned ?? []
const byArchived = list.filter((p) => (p.isArchived ?? false) === onlyShowArchived)
const byArchived = list.filter((p) => (onlyShowArchived ? !!p.isArchived : true))
if (onlyShowPinned) {
return byArchived.filter((p) => p.id != null && pinnedProjectIds.includes(p.id))
}
Expand Down Expand Up @@ -141,6 +149,18 @@ export const ManageProjectsProvider = ({ children }: { children: ReactNode }) =>
return map
}, [challengesListing])

const challengeMetaByProjectId = useMemo(() => {
const map: Record<number, ChallengeMeta> = {}
for (const c of challengesListing) {
const meta = map[c.parent] ?? { totalChallenges: 0, pinned: 0, completed: 0 }
map[c.parent] = meta
meta.totalChallenges += 1
if (c.status === CHALLENGE_STATUS_FINISHED) meta.completed += 1
if (pinnedChallengeIds.includes(c.id)) meta.pinned += 1
}
return map
}, [challengesListing, pinnedChallengeIds])

// All useCallback hooks below are stored in the context value — stable references
// prevent all context consumers from re-rendering on every provider render.
const handleExportCsv = useCallback(
Expand Down Expand Up @@ -232,6 +252,7 @@ export const ManageProjectsProvider = ({ children }: { children: ReactNode }) =>
pinnedProjectIds,
toggleProjectPin,
challengeCountsByProjectId,
challengeMetaByProjectId,
updateProject,
handleExportCsv,
handleArchiveProject,
Expand All @@ -257,6 +278,7 @@ export const ManageProjectsProvider = ({ children }: { children: ReactNode }) =>
pinnedProjectIds,
toggleProjectPin,
challengeCountsByProjectId,
challengeMetaByProjectId,
updateProject,
handleExportCsv,
handleArchiveProject,
Expand Down
41 changes: 19 additions & 22 deletions src/components/shared/ProjectCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Link } from '@tanstack/react-router'
import { useEffect, useState } from 'react'
import { useIntl } from '@/i18n'
import { formatDate } from '@/lib/date'
import { STATUS_HEX, STATUS_KEY_TO_ID } from '@/lib/taskConstants'
import { cn } from '@/lib/utils'
import type { Project } from '@/types/Project'
import { ProgressBar } from './ProgressBar'
Expand Down Expand Up @@ -68,11 +69,7 @@ export const ProjectCard = ({
)}
<SidebarIndicator avatarUrl={project.avatarUrl} onLoadFailure={() => setImageFailed(true)} />
<div className="p-4">
<div
className={cn('mb-2 text-xs text-zinc-500 dark:text-slate-300', showsImage && 'mr-16')}
>
{project.displayName || project.name}
</div>
<div className="mb-2 h-4" />

<h3
className={cn(
Expand All @@ -88,28 +85,28 @@ export const ProjectCard = ({
<span className="font-semibold text-zinc-900 dark:text-white">
{meta.totalChallenges - meta.completed} / {meta.totalChallenges}
</span>{' '}
{t('common.tasksRemaining2', undefined, 'tasks remaining')}
{t('shared.projectCard.challengesRemaining', undefined, 'challenges remaining')}
</div>

<ProgressBar percentage={completionPercentage} className="mb-3" />
<ProgressBar
segments={[
{
key: 'completed',
percentage: completionPercentage,
color: STATUS_HEX[STATUS_KEY_TO_ID.fixed],
},
]}
className="mb-3"
/>

<div className="flex items-center justify-between gap-3 text-xs text-zinc-500 dark:text-slate-300">
<span className="flex items-center gap-3">
<span>
{t(
'shared.projectCard.totalChallenges',
{ count: meta.totalChallenges },
'Total Challenges: {count}'
<span className="text-yellow-500">
{meta.pinned > 0 &&
t(
'shared.projectCard.pinnedChallenges',
{ count: meta.pinned },
'{count, plural, one {# pinned challenge} other {# pinned challenges}}'
)}
</span>
<span className="text-emerald-500">
{t('shared.projectCard.completed', { count: meta.completed }, 'Completed: {count}')}
</span>
{meta.pinned > 0 && (
<span className="text-yellow-500">
{t('shared.projectCard.pinned', { count: meta.pinned }, 'Pinned: {count}')}
</span>
)}
</span>
{lastUpdated ? (
<span>
Expand Down
6 changes: 2 additions & 4 deletions src/i18n/messages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,6 @@
"common.tasks": "Tasks",
"common.tasks2": "tasks",
"common.tasksRemaining": "Tasks remaining",
"common.tasksRemaining2": "tasks remaining",
"common.tasksWithCount": "{count, plural, one {# task} other {# tasks}}",
"common.team": "Team #{teamId}",
"common.teamNotFound": "Team not found.",
Expand Down Expand Up @@ -1034,9 +1033,8 @@
"shared.notFound.backHome": "Head back home",
"shared.notFound.description": "The page you are looking for does not exist.",
"shared.pointsTicker.viewProfile": "{score} points — view profile",
"shared.projectCard.completed": "Completed: {count}",
"shared.projectCard.pinned": "Pinned: {count}",
"shared.projectCard.totalChallenges": "Total Challenges: {count}",
"shared.projectCard.challengesRemaining": "challenges remaining",
"shared.projectCard.pinnedChallenges": "{count, plural, one {# pinned challenge} other {# pinned challenges}}",
"shared.projectPickerModal.description": "Choose the project this challenge belongs to.",
"shared.projectPickerModal.noProjectsFound": "No projects found.",
"shared.searchBar.placeholder": "Search...",
Expand Down
Loading