Skip to content

Commit b322dc2

Browse files
committed
added mobile view and resolves comments
1 parent fd72300 commit b322dc2

File tree

5 files changed

+166
-86
lines changed

5 files changed

+166
-86
lines changed

backend/apps/github/api/internal/nodes/issue.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ def labels(self) -> list[str]:
5151
"""Resolve label names for the issue."""
5252
return list(self.labels.values_list("name", flat=True))
5353

54+
@strawberry.field
55+
def is_merged(self) -> bool:
56+
"""Return True if this issue has at least one merged pull request."""
57+
return self.pull_requests.filter(state="closed", merged_at__isnull=False).exists()
58+
5459
@strawberry.field
5560
def interested_users(self) -> list[UserNode]:
5661
"""Return all users who have expressed interest in this issue."""
@@ -65,3 +70,4 @@ def interested_users(self) -> list[UserNode]:
6570
def pull_requests(self) -> list[PullRequestNode]:
6671
"""Return all pull requests linked to this issue."""
6772
return list(self.pull_requests.select_related("author", "repository").all())
73+

frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx

Lines changed: 64 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,26 @@ import { TruncatedText } from 'components/TruncatedText'
3232
const ModuleIssueDetailsPage = () => {
3333
const params = useParams() as { programKey: string; moduleKey: string; issueId: string }
3434
const { programKey, moduleKey, issueId } = params
35+
36+
const formatDeadline = (deadline: string | null) => {
37+
if (!deadline) return { text: 'No deadline set', color: 'text-gray-600 dark:text-gray-300' }
38+
39+
const deadlineDate = new Date(deadline)
40+
const today = new Date()
41+
const isOverdue = deadlineDate < today
42+
const daysLeft = Math.ceil((deadlineDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
43+
44+
const statusText = isOverdue
45+
? '(overdue)'
46+
: daysLeft === 0
47+
? '(today)'
48+
: `(${daysLeft} days left)`
49+
50+
return {
51+
text: `${deadlineDate.toLocaleDateString()} ${statusText}`,
52+
color: 'text-[#DA3633]',
53+
}
54+
}
3555
const { data, loading, error } = useQuery(GET_MODULE_ISSUE_VIEW, {
3656
variables: { programKey, moduleKey, number: Number(issueId) },
3757
skip: !issueId,
@@ -167,10 +187,14 @@ const ModuleIssueDetailsPage = () => {
167187
</span>
168188
<span
169189
className={`inline-flex items-center rounded-lg px-2 py-0.5 text-xs font-medium ${
170-
issue.state === 'open' ? 'bg-[#2cbe4e] text-white' : 'bg-[#cb2431] text-white'
190+
issue.state === 'open'
191+
? 'bg-[#238636] text-white'
192+
: issue.isMerged
193+
? 'bg-[#8657E5] text-white'
194+
: 'bg-[#DA3633] text-white'
171195
}`}
172196
>
173-
{issue.state === 'open' ? 'Open' : 'Closed'}
197+
{issue.state === 'open' ? 'Open' : issue.isMerged ? 'Merged' : 'Closed'}
174198
</span>
175199
</div>
176200
</div>
@@ -195,23 +219,18 @@ const ModuleIssueDetailsPage = () => {
195219
</div>
196220

197221
<div className="flex flex-col gap-2">
198-
<div className="flex flex-wrap items-center gap-2">
199-
<span className="font-medium">Deadline:</span>
222+
<div className="flex flex-wrap items-center">
223+
<span className="font-medium">Deadline: </span>
200224
{isEditingDeadline && canEditDeadline ? (
201-
<span className="inline-flex items-center gap-2">
202-
<input
203-
type="date"
204-
value={deadlineInput}
205-
onChange={(e) => setDeadlineInput(e.target.value)}
206-
min={new Date().toISOString().slice(0, 10)}
207-
className="h-8 rounded border border-gray-300 px-2 text-xs dark:border-gray-600"
208-
/>
209-
<button
210-
type="button"
211-
disabled={!deadlineInput || settingDeadline}
212-
onClick={async () => {
213-
if (!deadlineInput || settingDeadline || !issueId) return
214-
const localDate = new Date(deadlineInput + 'T23:59:59')
225+
<input
226+
type="date"
227+
value={deadlineInput}
228+
onChange={async (e) => {
229+
const newValue = e.target.value
230+
setDeadlineInput(newValue)
231+
232+
if (newValue && !settingDeadline && issueId) {
233+
const localDate = new Date(newValue + 'T23:59:59')
215234
const iso = localDate.toISOString()
216235
await setTaskDeadlineMutation({
217236
variables: {
@@ -221,64 +240,35 @@ const ModuleIssueDetailsPage = () => {
221240
deadlineAt: iso,
222241
},
223242
})
224-
}}
225-
className="flex items-center justify-center rounded-md border border-[#1D7BD7] px-3 py-1 text-xs font-medium text-[#1D7BD7] transition-all hover:bg-[#1D7BD7] hover:text-white disabled:cursor-not-allowed disabled:opacity-50"
226-
>
227-
{settingDeadline ? 'Saving…' : 'Save'}
228-
</button>
229-
<button
230-
type="button"
231-
aria-label="Cancel deadline edit"
232-
title="Cancel"
233-
onClick={() => {
234-
setDeadlineInput(
235-
taskDeadline ? new Date(taskDeadline).toISOString().slice(0, 10) : ''
236-
)
237243
setIsEditingDeadline(false)
238-
}}
239-
className="inline-flex h-7 w-7 items-center justify-center rounded-md text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
240-
>
241-
<FontAwesomeIcon icon={faXmark} className="h-3.5 w-3.5" />
242-
</button>
243-
</span>
244+
}
245+
}}
246+
min={new Date().toISOString().slice(0, 10)}
247+
className="h-8 rounded border border-gray-300 px-2 text-xs dark:border-gray-600"
248+
/>
244249
) : (
245-
<span className="inline-flex items-center gap-2">
246-
{(() => {
247-
if (!taskDeadline) {
248-
return (
249-
<span className="text-xs font-medium text-gray-600 dark:text-gray-300">
250-
No deadline set
251-
</span>
250+
<button
251+
type="button"
252+
disabled={!canEditDeadline}
253+
onClick={() => {
254+
if (canEditDeadline) {
255+
setDeadlineInput(
256+
taskDeadline ? new Date(taskDeadline).toISOString().slice(0, 10) : ''
252257
)
258+
setIsEditingDeadline(true)
253259
}
254-
const d = new Date(taskDeadline)
255-
const today = new Date()
256-
const isPast = d.setHours(0, 0, 0, 0) < today.setHours(0, 0, 0, 0)
257-
const diffDays = Math.ceil(
258-
(d.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)
259-
)
260-
const textClass = 'text-red-700 dark:text-red-300'
261-
return (
262-
<span className={`text-xs font-medium ${textClass}`}>
263-
{d.toLocaleDateString()}{' '}
264-
{isPast
265-
? '(overdue)'
266-
: diffDays > 0
267-
? `(in ${diffDays} days)`
268-
: '(today)'}
269-
</span>
270-
)
260+
}}
261+
className={`inline-flex items-center gap-2 rounded px-2 py-1 text-left transition-colors ${
262+
canEditDeadline
263+
? 'hover:bg-gray-100 dark:hover:bg-gray-800'
264+
: 'cursor-not-allowed'
265+
}`}
266+
>
267+
{(() => {
268+
const { text, color } = formatDeadline(taskDeadline)
269+
return <span className={`text-xs font-medium ${color}`}>{text}</span>
271270
})()}
272-
{canEditDeadline && (
273-
<button
274-
type="button"
275-
className="ml-2 text-xs text-blue-600 hover:underline dark:text-blue-400"
276-
onClick={() => setIsEditingDeadline(true)}
277-
>
278-
Edit
279-
</button>
280-
)}
281-
</span>
271+
</button>
282272
)}
283273
</div>
284274
</div>
@@ -407,21 +397,21 @@ const ModuleIssueDetailsPage = () => {
407397
{pr.state === 'closed' && pr.mergedAt ? (
408398
<span
409399
className="inline-flex items-center rounded-lg px-2 py-0.5 text-xs font-medium text-white"
410-
style={{ backgroundColor: '#6f42c1' }}
400+
style={{ backgroundColor: '#8657E5' }}
411401
>
412402
Merged
413403
</span>
414404
) : pr.state === 'closed' ? (
415405
<span
416406
className="inline-flex items-center rounded-lg px-2 py-0.5 text-xs font-medium text-white"
417-
style={{ backgroundColor: '#cb2431' }}
407+
style={{ backgroundColor: '#DA3633' }}
418408
>
419409
Closed
420410
</span>
421411
) : (
422412
<span
423413
className="inline-flex items-center rounded-lg px-2 py-0.5 text-xs font-medium text-white"
424-
style={{ backgroundColor: '#2cbe4e' }}
414+
style={{ backgroundColor: '#238636' }}
425415
>
426416
Open
427417
</span>

frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx

Lines changed: 94 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const IssuesPage = () => {
4444
number: number
4545
title: string
4646
state: string
47+
isMerged: boolean
4748
labels: string[]
4849
assignees: Array<{ avatarUrl: string; login: string; name: string }>
4950
}
@@ -54,6 +55,7 @@ const IssuesPage = () => {
5455
number: i.number,
5556
title: i.title,
5657
state: i.state,
58+
isMerged: i.isMerged,
5759
labels: i.labels || [],
5860
assignees: i.assignees || [],
5961
}))
@@ -135,7 +137,8 @@ const IssuesPage = () => {
135137
</div>
136138
</div>
137139

138-
<div className="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
140+
{/* Desktop Table - unchanged */}
141+
<div className="hidden overflow-hidden rounded-lg border border-gray-200 lg:block dark:border-gray-700">
139142
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
140143
<thead className="bg-gray-50 dark:bg-[#2a2e33]">
141144
<tr>
@@ -186,16 +189,20 @@ const IssuesPage = () => {
186189
</button>
187190
</Tooltip>
188191
</td>
189-
<td className="px-6 py-4 text-sm whitespace-nowrap">
190-
<span
191-
className={`inline-flex items-center rounded-lg px-2 py-0.5 text-xs font-medium ${
192-
issue.state === 'open'
193-
? 'bg-[#2cbe4e] text-white'
194-
: 'bg-[#cb2431] text-white'
195-
}`}
196-
>
197-
{issue.state === 'open' ? 'Open' : 'Closed'}
198-
</span>
192+
<td className="px-6 py-4 text-center text-sm whitespace-nowrap">
193+
<div className="flex justify-center">
194+
<span
195+
className={`inline-flex items-center rounded-lg px-2 py-0.5 text-xs font-medium ${
196+
issue.state === 'open'
197+
? 'bg-[#238636] text-white'
198+
: issue.isMerged
199+
? 'bg-[#8657E5] text-white'
200+
: 'bg-[#DA3633] text-white'
201+
}`}
202+
>
203+
{issue.state === 'open' ? 'Open' : issue.isMerged ? 'Merged' : 'Closed'}
204+
</span>
205+
</div>
199206
</td>
200207
<td className="px-6 py-4">
201208
<div className="flex flex-wrap gap-2">
@@ -234,7 +241,9 @@ const IssuesPage = () => {
234241
alt={issue.assignees[0].login}
235242
className="rounded-full"
236243
/>
237-
<span>{issue.assignees[0].login || issue.assignees[0].name}</span>
244+
<span className="max-w-[80px] truncate sm:max-w-[100px] md:max-w-[120px] lg:max-w-[150px]">
245+
{issue.assignees[0].login || issue.assignees[0].name}
246+
</span>
238247
</div>
239248
{issue.assignees.length > 1 && (
240249
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-gray-200 text-xs font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-300">
@@ -260,6 +269,79 @@ const IssuesPage = () => {
260269
</table>
261270
</div>
262271

272+
{/* Mobile & Tablet Cards */}
273+
<div className="space-y-6 lg:hidden">
274+
{moduleIssues.map((issue) => (
275+
<div
276+
key={issue.objectID}
277+
className="rounded-lg border border-gray-200 bg-white p-4 mt-4 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-[#1f2327]"
278+
>
279+
<div className="mb-3 flex items-start justify-between gap-3">
280+
<button
281+
type="button"
282+
onClick={() => handleIssueClick(Number(issue.number))}
283+
className="flex-1 text-left text-sm font-medium text-gray-900 hover:text-blue-600 dark:text-gray-100 dark:hover:text-blue-400"
284+
>
285+
{issue.title}
286+
</button>
287+
<span
288+
className={`inline-flex flex-shrink-0 items-center rounded-full px-2 py-1 text-xs font-medium ${
289+
issue.state === 'open'
290+
? 'bg-[#238636] text-white'
291+
: issue.isMerged
292+
? 'bg-[#8657E5] text-white'
293+
: 'bg-[#DA3633] text-white'
294+
}`}
295+
>
296+
{issue.state === 'open' ? 'Open' : issue.isMerged ? 'Merged' : 'Closed'}
297+
</span>
298+
</div>
299+
300+
{issue.labels?.length > 0 && (
301+
<div className="mb-3 flex flex-wrap gap-1">
302+
{issue.labels.slice(0, 3).map((label) => (
303+
<span
304+
key={label}
305+
className="inline-flex items-center rounded-md bg-gray-100 px-2 py-0.5 text-xs text-gray-700 dark:bg-gray-800 dark:text-gray-300"
306+
>
307+
{label}
308+
</span>
309+
))}
310+
{issue.labels.length > 3 && (
311+
<span className="inline-flex items-center rounded-md bg-gray-100 px-2 py-0.5 text-xs text-gray-500 dark:bg-gray-800 dark:text-gray-400">
312+
+{issue.labels.length - 3}
313+
</span>
314+
)}
315+
</div>
316+
)}
317+
318+
{issue.assignees?.length > 0 && (
319+
<div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
320+
<Image
321+
height={18}
322+
width={18}
323+
src={issue.assignees[0].avatarUrl}
324+
alt={issue.assignees[0].login}
325+
className="rounded-full"
326+
/>
327+
<span className="truncate">
328+
{issue.assignees[0].login || issue.assignees[0].name}
329+
{issue.assignees.length > 1 && ` +${issue.assignees.length - 1}`}
330+
</span>
331+
</div>
332+
)}
333+
</div>
334+
))}
335+
336+
{moduleIssues.length === 0 && (
337+
<div className="rounded-lg border border-gray-200 bg-white p-6 text-center dark:border-gray-700 dark:bg-[#1f2327]">
338+
<p className="text-sm text-gray-500 dark:text-gray-400">
339+
No issues found for the selected filter.
340+
</p>
341+
</div>
342+
)}
343+
</div>
344+
263345
{/* Pagination Controls */}
264346
<Pagination
265347
currentPage={currentPage}

frontend/src/server/queries/issueQueries.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const GET_MODULE_ISSUE_VIEW = gql`
1313
body
1414
url
1515
state
16+
isMerged
1617
organizationName
1718
repositoryName
1819
assignees {

frontend/src/server/queries/moduleQueries.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export const GET_MODULE_ISSUES = gql`
9494
number
9595
title
9696
state
97+
isMerged
9798
labels
9899
assignees {
99100
avatarUrl

0 commit comments

Comments
 (0)