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

RepoCard.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.

RepoCard.tsxBlame168 lines · 1 contributor
9989d86Claude1import React, { useCallback } from 'react';
2import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
3import { colors } from '../theme/colors';
4import type { Repository } from '../api/repos';
5
6interface RepoCardProps {
7 repo: Repository;
8 onPress: (repo: Repository) => 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 RepoCard({ repo, onPress }: RepoCardProps): React.ReactElement {
26 const handlePress = useCallback(() => onPress(repo), [repo, onPress]);
27
28 return (
29 <TouchableOpacity
30 style={styles.card}
31 onPress={handlePress}
32 activeOpacity={0.75}
33 >
34 <View style={styles.header}>
35 <View style={styles.titleRow}>
36 <Text style={styles.repoName} numberOfLines={1}>
37 {repo.ownerUsername}/{repo.name}
38 </Text>
39 {repo.isPrivate && (
40 <View style={styles.badge}>
41 <Text style={styles.badgeText}>Private</Text>
42 </View>
43 )}
44 {repo.isArchived && (
45 <View style={[styles.badge, styles.badgeArchived]}>
46 <Text style={styles.badgeText}>Archived</Text>
47 </View>
48 )}
49 {repo.isFork && (
50 <View style={[styles.badge, styles.badgeFork]}>
51 <Text style={styles.badgeText}>Fork</Text>
52 </View>
53 )}
54 </View>
55 {repo.description !== null && repo.description.length > 0 && (
56 <Text style={styles.description} numberOfLines={2}>
57 {repo.description}
58 </Text>
59 )}
60 </View>
61
62 <View style={styles.footer}>
63 {repo.language !== null && (
64 <View style={styles.metaItem}>
65 <View style={styles.langDot} />
66 <Text style={styles.metaText}>{repo.language}</Text>
67 </View>
68 )}
69 <View style={styles.metaItem}>
70 <Text style={styles.metaIcon}>★</Text>
71 <Text style={styles.metaText}>{repo.starCount}</Text>
72 </View>
73 <View style={styles.metaItem}>
74 <Text style={styles.metaIcon}>⑂</Text>
75 <Text style={styles.metaText}>{repo.forkCount}</Text>
76 </View>
77 {repo.openIssueCount > 0 && (
78 <View style={styles.metaItem}>
79 <Text style={styles.metaIcon}>○</Text>
80 <Text style={styles.metaText}>{repo.openIssueCount}</Text>
81 </View>
82 )}
83 <Text style={styles.updatedAt}>Updated {formatRelative(repo.updatedAt)}</Text>
84 </View>
85 </TouchableOpacity>
86 );
87}
88
89const styles = StyleSheet.create({
90 card: {
91 backgroundColor: colors.bgSecondary,
92 borderRadius: 8,
93 borderWidth: 1,
94 borderColor: colors.border,
95 padding: 16,
96 marginHorizontal: 16,
97 marginBottom: 12,
98 },
99 header: {
100 marginBottom: 12,
101 gap: 6,
102 },
103 titleRow: {
104 flexDirection: 'row',
105 alignItems: 'center',
106 flexWrap: 'wrap',
107 gap: 8,
108 },
109 repoName: {
110 fontSize: 15,
111 fontWeight: '600',
112 color: colors.textLink,
113 flexShrink: 1,
114 },
115 description: {
116 fontSize: 13,
117 color: colors.textMuted,
118 lineHeight: 18,
119 },
120 badge: {
121 paddingHorizontal: 7,
122 paddingVertical: 2,
123 borderRadius: 20,
124 backgroundColor: colors.bgTertiary,
125 borderWidth: 1,
126 borderColor: colors.border,
127 },
128 badgeArchived: {
129 borderColor: colors.accentYellow,
130 },
131 badgeFork: {
132 borderColor: colors.accentBlue,
133 },
134 badgeText: {
135 fontSize: 11,
136 color: colors.textMuted,
137 },
138 footer: {
139 flexDirection: 'row',
140 alignItems: 'center',
141 flexWrap: 'wrap',
142 gap: 12,
143 },
144 metaItem: {
145 flexDirection: 'row',
146 alignItems: 'center',
147 gap: 4,
148 },
149 langDot: {
150 width: 10,
151 height: 10,
152 borderRadius: 5,
153 backgroundColor: colors.accentPurple,
154 },
155 metaIcon: {
156 fontSize: 12,
157 color: colors.textMuted,
158 },
159 metaText: {
160 fontSize: 12,
161 color: colors.textMuted,
162 },
163 updatedAt: {
164 fontSize: 12,
165 color: colors.textMuted,
166 marginLeft: 'auto',
167 },
168});