+2
-2
dist/index.html
+2
-2
dist/index.html
···
26
26
content="black-translucent"
27
27
/>
28
28
<title>ATLast: Find Your People in the ATmosphere</title>
29
-
<script type="module" crossorigin src="/assets/index-o15l4btH.js"></script>
30
-
<link rel="stylesheet" crossorigin href="/assets/index-Bj_zheer.css">
29
+
<script type="module" crossorigin src="/assets/index-DCjXuvAz.js"></script>
30
+
<link rel="stylesheet" crossorigin href="/assets/index-BxAsnHb8.css">
31
31
</head>
32
32
<body>
33
33
<div id="root"></div>
+56
-45
src/components/HistoryTab.tsx
+56
-45
src/components/HistoryTab.tsx
···
10
10
import SetupPrompt from "./common/SetupPrompt";
11
11
import Card from "./common/Card";
12
12
import Badge from "./common/Badge";
13
+
import CardItem from "./common/CardItem";
13
14
14
15
interface HistoryTabProps {
15
16
uploads: UploadType[];
···
93
94
<Card
94
95
key={upload.uploadId}
95
96
variant="upload"
96
-
onClick={() => onLoadUpload(upload.uploadId)}
97
-
className="w-full flex items-start space-x-4 p-4"
97
+
className="w-full"
98
98
>
99
-
<div
100
-
className={`w-10 h-10 bg-gradient-to-r ${getPlatformColor(upload.sourcePlatform)} rounded-xl flex items-center justify-center flex-shrink-0 shadow-md`}
101
-
>
102
-
<Sparkles className="w-6 h-6 text-white" />
103
-
</div>
104
-
<div className="flex-1 min-w-0">
105
-
<div className="flex flex-wrap items-start justify-between gap-x-4 gap-y-2 mb-1">
106
-
<div className="font-semibold text-purple-950 dark:text-cyan-50 capitalize leading-tight">
107
-
{upload.sourcePlatform}
108
-
</div>
109
-
<div className="flex items-center gap-2 flex-shrink-0">
110
-
<span className="text-sm text-purple-750 dark:text-cyan-250 whitespace-nowrap flex-shrink-0">
111
-
{upload.matchedUsers}{" "}
112
-
{upload.matchedUsers === 1 ? "match" : "matches"}
113
-
</span>
114
-
</div>
115
-
</div>
116
-
{destApp && (
117
-
<a
118
-
href={destApp.link}
119
-
target="_blank"
120
-
rel="noopener noreferrer"
121
-
onClick={(e) => e.stopPropagation()}
122
-
className="text-sm text-purple-750 dark:text-cyan-250 hover:underline leading-tight flex items-center space-x-1 w-fit"
99
+
<CardItem
100
+
padding="p-4"
101
+
badgeIndentClass="sm:pl-[56px]"
102
+
onClick={() => onLoadUpload(upload.uploadId)}
103
+
avatar={
104
+
<div
105
+
className={`w-10 h-10 bg-gradient-to-r ${getPlatformColor(upload.sourcePlatform)} rounded-xl flex items-center justify-center flex-shrink-0 shadow-md`}
123
106
>
124
-
<span>{destApp.action} on</span>
107
+
<Sparkles className="w-6 h-6 text-white" />
108
+
</div>
109
+
}
110
+
content={
111
+
<>
112
+
<div className="flex flex-wrap items-start justify-between gap-x-4 gap-y-2">
113
+
<div className="font-semibold text-purple-950 dark:text-cyan-50 capitalize leading-tight">
114
+
{upload.sourcePlatform}
115
+
</div>
116
+
<div className="flex items-center gap-2 flex-shrink-0">
117
+
<span className="text-sm text-purple-750 dark:text-cyan-250 whitespace-nowrap flex-shrink-0">
118
+
{upload.matchedUsers}{" "}
119
+
{upload.matchedUsers === 1 ? "match" : "matches"}
120
+
</span>
121
+
</div>
122
+
</div>
123
+
{destApp && (
124
+
<a
125
+
href={destApp.link}
126
+
target="_blank"
127
+
rel="noopener noreferrer"
128
+
onClick={(e) => e.stopPropagation()}
129
+
className="text-sm text-purple-750 dark:text-cyan-250 hover:underline leading-tight flex items-center space-x-1 w-fit"
130
+
>
131
+
<span>{destApp.action} on</span>
125
132
126
-
<FaviconIcon
127
-
url={destApp.icon}
128
-
alt={destApp.name}
129
-
className="w-3 h-3 mb-0.5 flex-shrink-0"
130
-
/>
133
+
<FaviconIcon
134
+
url={destApp.icon}
135
+
alt={destApp.name}
136
+
className="w-3 h-3 mb-0.5 flex-shrink-0"
137
+
/>
131
138
132
-
<span>{destApp.name}</span>
133
-
</a>
134
-
)}
135
-
<div className="flex items-center flex-wrap gap-2 py-1.5 sm:ml-0 -ml-14">
136
-
<Badge variant="info">
137
-
{upload.totalUsers}{" "}
138
-
{upload.totalUsers === 1 ? "user found" : "users found"}
139
-
</Badge>
140
-
<Badge variant="info">
141
-
Uploaded {formatRelativeTime(upload.createdAt)}
142
-
</Badge>
143
-
</div>
144
-
</div>
139
+
<span>{destApp.name}</span>
140
+
</a>
141
+
)}
142
+
</>
143
+
}
144
+
badges={
145
+
<>
146
+
<Badge variant="info">
147
+
{upload.totalUsers}{" "}
148
+
{upload.totalUsers === 1 ? "user found" : "users found"}
149
+
</Badge>
150
+
<Badge variant="info">
151
+
Uploaded {formatRelativeTime(upload.createdAt)}
152
+
</Badge>
153
+
</>
154
+
}
155
+
/>
145
156
</Card>
146
157
);
147
158
})}
+23
-26
src/components/SearchResultCard.tsx
+23
-26
src/components/SearchResultCard.tsx
···
8
8
import Badge from "./common/Badge";
9
9
import { StatBadge } from "./common/Stats";
10
10
import Card from "./common/Card";
11
+
import CardItem from "./common/CardItem";
11
12
12
13
interface SearchResultCardProps {
13
14
result: SearchResult;
···
28
29
onToggle: () => void;
29
30
}>(({ match, isSelected, isFollowed, currentAppName, onToggle }) => {
30
31
return (
31
-
<div className="p-3 cursor-pointer hover:scale-[1.01] transition-transform">
32
-
{/* Top row: Avatar, Name/Handle, Follow Button */}
33
-
<div className="flex items-start gap-3 mb-2">
32
+
<CardItem
33
+
padding="p-3"
34
+
badgeIndentClass="sm:pl-[44px]"
35
+
avatar={
34
36
<AvatarWithFallback
35
37
avatar={match.avatar}
36
38
handle={match.handle || ""}
37
39
size="sm"
38
40
/>
39
-
40
-
<div className="flex-1 min-w-0">
41
+
}
42
+
content={
43
+
<>
41
44
{match.displayName && (
42
45
<div className="font-semibold text-purple-950 dark:text-cyan-50 leading-tight">
43
46
{match.displayName}
···
51
54
>
52
55
@{match.handle}
53
56
</a>
54
-
</div>
55
-
57
+
</>
58
+
}
59
+
action={
56
60
<FollowButton
57
61
isFollowed={isFollowed}
58
62
isSelected={isSelected}
59
63
onToggle={onToggle}
60
64
appName={currentAppName}
61
65
/>
62
-
</div>
63
-
64
-
{/* Stats/Badges - align with avatar on mobile (pl-[60px] = avatar width 48px + gap 12px) */}
65
-
<div className="flex items-center flex-wrap gap-2 pl-0 md:pl-[60px]">
66
-
{typeof match.postCount === "number" && match.postCount > 0 && (
67
-
<StatBadge value={match.postCount} label="posts" />
68
-
)}
69
-
{typeof match.followerCount === "number" &&
70
-
match.followerCount > 0 && (
66
+
}
67
+
badges={
68
+
<>
69
+
{typeof match.postCount === "number" && match.postCount > 0 && (
70
+
<StatBadge value={match.postCount} label="posts" />
71
+
)}
72
+
{typeof match.followerCount === "number" && match.followerCount > 0 && (
71
73
<StatBadge value={match.followerCount} label="followers" />
72
74
)}
73
-
<Badge variant="match">{match.matchScore}% match</Badge>
74
-
</div>
75
-
76
-
{/* Description - align with avatar on mobile */}
77
-
{match.description && (
78
-
<div className="text-sm text-purple-900 dark:text-cyan-100 line-clamp-2 pt-2 pl-0 md:pl-[60px]">
79
-
{match.description}
80
-
</div>
81
-
)}
82
-
</div>
75
+
<Badge variant="match">{match.matchScore}% match</Badge>
76
+
</>
77
+
}
78
+
description={match.description}
79
+
/>
83
80
);
84
81
});
85
82
+63
src/components/common/CardItem.tsx
+63
src/components/common/CardItem.tsx
···
1
+
import React from "react";
2
+
3
+
interface CardItemProps {
4
+
avatar: React.ReactNode;
5
+
content: React.ReactNode;
6
+
action?: React.ReactNode;
7
+
badges: React.ReactNode;
8
+
description?: React.ReactNode;
9
+
padding?: "p-3" | "p-4";
10
+
badgeIndentClass: string; // Responsive indent class, e.g., "sm:pl-[44px]"
11
+
onClick?: () => void;
12
+
}
13
+
14
+
/**
15
+
* Shared card item component for consistent layout structure.
16
+
*
17
+
* Structure:
18
+
* - Top row: Avatar + Content + Optional Action (flex layout)
19
+
* - Badge row: With responsive left indent
20
+
* - Optional description row: With responsive left indent
21
+
*
22
+
* Used by SearchResultCard and HistoryTab for consistent spacing.
23
+
*/
24
+
const CardItem: React.FC<CardItemProps> = ({
25
+
avatar,
26
+
content,
27
+
action,
28
+
badges,
29
+
description,
30
+
padding = "p-3",
31
+
badgeIndentClass,
32
+
onClick,
33
+
}) => {
34
+
return (
35
+
<div
36
+
className={`${padding} cursor-pointer hover:scale-[1.01] transition-transform`}
37
+
onClick={onClick}
38
+
>
39
+
{/* Top row: Avatar + Content + Action */}
40
+
<div className="flex items-start gap-3 mb-1">
41
+
{avatar}
42
+
<div className="flex-1 min-w-0">{content}</div>
43
+
{action}
44
+
</div>
45
+
46
+
{/* Badges row - responsive indent based on avatar size */}
47
+
<div className={`flex items-center flex-wrap gap-2 pl-0 ${badgeIndentClass}`}>
48
+
{badges}
49
+
</div>
50
+
51
+
{/* Optional description - same responsive indent as badges */}
52
+
{description && (
53
+
<div
54
+
className={`text-sm text-purple-900 dark:text-cyan-100 line-clamp-2 mt-1 pt-2 pl-1 ${badgeIndentClass}`}
55
+
>
56
+
{description}
57
+
</div>
58
+
)}
59
+
</div>
60
+
);
61
+
};
62
+
63
+
export default CardItem;