Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

IssueRow.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

IssueRow.tsxBlame140 lines · 1 contributor
9989d86Claude1import React, { useCallback } from 'react';
2import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
3import { colors } from '../theme/colors';
4import type { Issue } from '../api/issues';
5
6interface IssueRowProps {
7 issue: Issue;
8 onPress: (issue: Issue) => void;
9}
10
11function formatRelative(dateStr: string): string {
12 const diff = Date.now() - new Date(dateStr).getTime();
13 const mins = Math.floor(diff / 60000);
14 if (mins < 1) return 'just now';
15 if (mins < 60) return `${mins}m ago`;
16 const hrs = Math.floor(mins / 60);
17 if (hrs < 24) return `${hrs}h ago`;
18 const days = Math.floor(hrs / 24);
19 if (days < 30) return `${days}d ago`;
20 const months = Math.floor(days / 30);
21 if (months < 12) return `${months}mo ago`;
22 return `${Math.floor(months / 12)}y ago`;
23}
24
25export function IssueRow({ issue, onPress }: IssueRowProps): React.ReactElement {
26 const handlePress = useCallback(() => onPress(issue), [issue, onPress]);
27
28 const stateColor = issue.state === 'open' ? colors.accent : colors.accentRed;
29
30 return (
31 <TouchableOpacity
32 style={styles.row}
33 onPress={handlePress}
34 activeOpacity={0.75}
35 >
36 <View style={[styles.stateIndicator, { backgroundColor: stateColor }]} />
37
38 <View style={styles.content}>
39 <View style={styles.titleRow}>
40 <Text style={styles.title} numberOfLines={2}>
41 {issue.title}
42 </Text>
43 </View>
44
45 {issue.labels.length > 0 && (
46 <View style={styles.labels}>
47 {issue.labels.slice(0, 4).map((label) => (
48 <View
49 key={label.id}
50 style={[styles.label, { borderColor: `#${label.color}` }]}
51 >
52 <Text style={[styles.labelText, { color: `#${label.color}` }]}>
53 {label.name}
54 </Text>
55 </View>
56 ))}
57 </View>
58 )}
59
60 <View style={styles.meta}>
61 <Text style={styles.metaText}>
62 #{issue.number} opened {formatRelative(issue.createdAt)} by {issue.authorUsername}
63 </Text>
64 {issue.commentCount > 0 && (
65 <View style={styles.commentCount}>
66 <Text style={styles.metaIcon}>💬</Text>
67 <Text style={styles.metaText}>{issue.commentCount}</Text>
68 </View>
69 )}
70 </View>
71 </View>
72 </TouchableOpacity>
73 );
74}
75
76const styles = StyleSheet.create({
77 row: {
78 flexDirection: 'row',
79 backgroundColor: colors.bgSecondary,
80 borderBottomWidth: 1,
81 borderBottomColor: colors.border,
82 paddingVertical: 12,
83 paddingHorizontal: 16,
84 gap: 12,
85 },
86 stateIndicator: {
87 width: 14,
88 height: 14,
89 borderRadius: 7,
90 marginTop: 3,
91 flexShrink: 0,
92 },
93 content: {
94 flex: 1,
95 gap: 6,
96 },
97 titleRow: {
98 flexDirection: 'row',
99 alignItems: 'flex-start',
100 },
101 title: {
102 fontSize: 14,
103 fontWeight: '500',
104 color: colors.text,
105 lineHeight: 20,
106 flex: 1,
107 },
108 labels: {
109 flexDirection: 'row',
110 flexWrap: 'wrap',
111 gap: 6,
112 },
113 label: {
114 paddingHorizontal: 7,
115 paddingVertical: 2,
116 borderRadius: 20,
117 borderWidth: 1,
118 },
119 labelText: {
120 fontSize: 11,
121 fontWeight: '500',
122 },
123 meta: {
124 flexDirection: 'row',
125 alignItems: 'center',
126 justifyContent: 'space-between',
127 },
128 metaText: {
129 fontSize: 12,
130 color: colors.textMuted,
131 },
132 commentCount: {
133 flexDirection: 'row',
134 alignItems: 'center',
135 gap: 3,
136 },
137 metaIcon: {
138 fontSize: 11,
139 },
140});