+48
-5
CONTRIBUTING.md
+48
-5
CONTRIBUTING.md
···
166
166
```
167
167
atlast/
168
168
├── src/
169
-
│ ├── components/ # React components
169
+
│ ├── assets/ # Logo
170
+
│ ├── components/ # UI components (React)
171
+
│ ├── constants/ #
170
172
│ ├── pages/ # Page components
171
173
│ ├── hooks/ # Custom hooks
172
174
│ ├── lib/
173
175
│ │ ├── apiClient/ # API client (real + mock)
174
-
│ │ ├── platforms/ # File parsers
176
+
│ │ ├── fileExtractor.ts # Chooses parser, handles file upload and data extraction
177
+
│ │ ├── parserLogic.ts # Parses file for usernames
178
+
│ │ ├── platformDefinitions.ts # File types and username locations
175
179
│ │ └── config.ts # Environment config
176
180
│ └── types/ # TypeScript types
177
181
├── netlify/
178
182
│ └── functions/ # Backend API
179
-
├── scripts/ # Build scripts
180
-
└── test-data/ # Sample upload files (git-ignored)
183
+
└── public/ #
181
184
```
185
+
186
+
### UI Color System
187
+
188
+
| **Element** | **Light Mode** | **Dark Mode** | **Notes** |
189
+
|:---:|:---:|:---:|:---:|
190
+
| Text Primary | purple-950 | cyan-50 | Headings, labels |
191
+
| Text Secondary | purple-750 | cyan-250 | Body text, descriptions |
192
+
| Text Tertiary | purple-600 | cyan-400 | Metadata, hints, icons |
193
+
| Borders (Rest) | cyan-500/30 | purple-500/30 | Cards, inputs default |
194
+
| Borders (Hover) | cyan-400 | purple-400 | Interactive hover |
195
+
| Borders (Active/Selected) | cyan-500 | purple-500 | Active tabs, selected items |
196
+
| Backgrounds (Primary) | white | slate-900 | Modal/card base |
197
+
| Backgrounds (Secondary) | purple-50 | slate-900 (nested sections) | Nested cards, sections |
198
+
| Backgrounds (Selected) | cyan-50 | purple-950/30 | Selected platform cards |
199
+
| Buttons Primary | orange-600 | orange-600 | CTAs |
200
+
| Buttons Primary Hover | orange-500 | orange-500 | CTA hover |
201
+
| Buttons Secondary | slate-600 | slate-700 | Cancel, secondary actions |
202
+
| Buttons Secondary Hover | slate-700 | slate-600 | Secondary hover |
203
+
| Interactive Selected | bg-cyan-50 border-cyan-500 | bg-purple-950/30 border-purple-500 | Platform selection cards |
204
+
| Accent/Badge | orange-500 | orange-500 (or amber-500) | Match counts, checkmarks, progress |
205
+
| Progress Complete | orange-500 | orange-500 | Completed progress bars |
206
+
| Progress Incomplete | cyan-500/30 | purple-500/30 | Incomplete progress bars |
207
+
| Success/Green | green-100/800 | green-900/300 | Followed status |
208
+
| Error/Red | red-600 | red-400 | Logout, errors |
209
+
210
+
### UI Color System: Patterns
211
+
**Disabled States**:
212
+
- Light: Reduce opacity to 50%, use purple-500/50
213
+
- Dark: Reduce opacity to 50%, use cyan-500/50
214
+
215
+
**Success/Match indicators**:
216
+
Both modes: amber-* or orange-* backgrounds with accessible text contrast
217
+
218
+
**Tab Navigation**:
219
+
- Inactive: Use text secondary colors
220
+
- Active border: orange-500 (light), amber-500 (dark)
221
+
- Active text: orange-650 (light), amber-400 (dark)
222
+
223
+
**Gradient Banners**:
224
+
- Both modes: from-amber-* via-orange-* to-pink-* (keep dramatic, adjust shades for mode)
182
225
183
226
---
184
227
···
250
293
251
294
---
252
295
253
-
Thank you for contributing to ATlast!
296
+
Thank you for contributing to ATlast!
+16
-17
src/components/AppHeader.tsx
+16
-17
src/components/AppHeader.tsx
···
46
46
}, []);
47
47
48
48
return (
49
-
<div className="bg-white/50 dark:bg-slate-900/50 backdrop-blur-xl relative z-[100]">
49
+
<div className="bg-white dark:bg-slate-900 border-b-2 border-cyan-500/30 dark:border-purple-500/30 backdrop-blur-xl relative z-[100]">
50
50
<div className="max-w-6xl mx-auto px-4 py-1">
51
51
<div className="flex items-center justify-between">
52
52
<button
53
53
onClick={() => onNavigate(session ? "home" : "login")}
54
-
className="flex items-center space-x-3 hover:opacity-80 transition-opacity focus:outline-none focus:ring-2 focus:ring-orange-500 rounded-lg px-2 py-1"
54
+
className="flex items-center space-x-3 hover:opacity-80 transition-opacity focus:outline-none focus:ring-2 focus:ring-orange-500 dark:focus:ring-amber-400 rounded-lg px-2 py-1"
55
55
>
56
-
<FireflyLogo className="w-12 h-12" />
56
+
<FireflyLogo className="w-14 h-10" />
57
57
<h1 className="font-display text-2xl font-bold text-purple-950 dark:text-cyan-50">
58
58
ATlast
59
59
</h1>
···
72
72
<div className="relative z-[9999]" ref={menuRef}>
73
73
<button
74
74
onClick={() => setShowMenu(!showMenu)}
75
-
className="flex items-center space-x-3 px-3 py-1 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors focus:outline-none focus:ring-2 focus:ring-firefly-orange"
75
+
className="flex items-center space-x-3 px-3 py-1 rounded-lg hover:bg-purple-50 dark:hover:bg-slate-800 transition-colors focus:outline-none focus:ring-2 focus:ring-orange-500 dark:focus:ring-amber-400"
76
76
>
77
77
{session?.avatar ? (
78
78
<img
···
81
81
className="w-8 h-8 rounded-full object-cover"
82
82
/>
83
83
) : (
84
-
<div className="w-8 h-8 bg-gradient-to-br from-cyan-500 to-purple-500 rounded-full flex items-center justify-center shadow-sm">
84
+
<div className="w-8 h-8 bg-gradient-to-br from-cyan-400 to-purple-500 rounded-full flex items-center justify-center shadow-sm">
85
85
<span className="text-white font-bold text-sm">
86
86
{session?.handle?.charAt(0).toUpperCase()}
87
87
</span>
···
91
91
@{session?.handle}
92
92
</span>
93
93
<ChevronDown
94
-
className={`w-4 h-4 text-slate-600 dark:text-slate-400 transition-transform ${showMenu ? "rotate-180" : ""}`}
94
+
className={`w-4 h-4 text-purple-750 dark:text-cyan-250 transition-transform ${showMenu ? "rotate-180" : ""}`}
95
95
/>
96
96
</button>
97
97
98
98
{showMenu && (
99
-
<div className="absolute right-0 mt-2 w-64 bg-white dark:bg-slate-800 rounded-lg shadow-lg border-2 border-cyan-500/30 dark:border-purple-500/30 py-2 z-[9999]">
99
+
<div className="absolute right-0 mt-2 w-64 bg-white dark:bg-slate-900 rounded-lg shadow-lg border-2 border-cyan-500/30 dark:border-purple-500/30 py-2 z-[9999]">
100
100
<div className="px-4 py-3">
101
101
<div className="font-semibold text-purple-950 dark:text-cyan-50">
102
102
{session?.displayName || session.handle}
103
103
</div>
104
-
<div className="text-sm text-slate-600 dark:text-slate-400">
104
+
<div className="text-sm text-purple-750 dark:text-cyan-250">
105
105
@{session?.handle}
106
106
</div>
107
107
</div>
···
110
110
setShowMenu(false);
111
111
onNavigate("home");
112
112
}}
113
-
className="w-full flex items-center space-x-3 px-4 py-2 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors text-left"
113
+
className="w-full flex items-center space-x-3 px-4 py-2 hover:bg-purple-50 dark:hover:bg-slate-800 transition-colors text-left"
114
114
>
115
-
<Home className="w-4 h-4 text-slate-600 dark:text-slate-400" />
116
-
<span className="text-slate-900 dark:text-slate-100">
115
+
<Home className="w-4 h-4 text-purple-950 dark:text-cyan-50" />
116
+
<span className="text-purple-950 dark:text-cyan-50">
117
117
Dashboard
118
118
</span>
119
119
</button>
···
122
122
setShowMenu(false);
123
123
onNavigate("login");
124
124
}}
125
-
className="w-full flex items-center space-x-3 px-4 py-2 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors text-left"
125
+
className="w-full flex items-center space-x-3 px-4 py-2 hover:bg-purple-50 dark:hover:bg-slate-800 transition-colors text-left"
126
126
>
127
-
<Heart className="w-4 h-4 text-slate-600 dark:text-slate-400" />
128
-
<span className="text-slate-900 dark:text-slate-100">
129
-
About
127
+
<Heart className="w-4 h-4 text-purple-950 dark:text-cyan-50" />
128
+
<span className="text-purple-950 dark:text-cyan-50">
129
+
Login screen
130
130
</span>
131
131
</button>
132
-
<div className="my-2"></div>
133
132
<button
134
133
onClick={() => {
135
134
setShowMenu(false);
136
135
onLogout();
137
136
}}
138
-
className="w-full flex items-center space-x-3 px-4 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors text-left text-red-600 dark:text-red-400"
137
+
className="w-full flex items-center space-x-3 px-4 py-2 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors text-left text-red-600 dark:text-red-400"
139
138
>
140
139
<LogOut className="w-4 h-4" />
141
140
<span>Log out</span>
+136
src/components/HistoryTab.tsx
+136
src/components/HistoryTab.tsx
···
1
+
import { Upload, Sparkles } from "lucide-react";
2
+
import { ATPROTO_APPS } from "../constants/atprotoApps";
3
+
import type { Upload as UploadType } from "../types";
4
+
import type { UserSettings } from "../types/settings";
5
+
6
+
interface HistoryTabProps {
7
+
uploads: UploadType[];
8
+
isLoading: boolean;
9
+
userSettings: UserSettings;
10
+
onLoadUpload: (uploadId: string) => void;
11
+
}
12
+
13
+
export default function HistoryTab({
14
+
uploads,
15
+
isLoading,
16
+
userSettings,
17
+
onLoadUpload,
18
+
}: HistoryTabProps) {
19
+
const formatDate = (dateString: string) => {
20
+
const date = new Date(dateString);
21
+
return date.toLocaleDateString("en-US", {
22
+
month: "short",
23
+
day: "numeric",
24
+
year: "numeric",
25
+
hour: "2-digit",
26
+
minute: "2-digit",
27
+
});
28
+
};
29
+
30
+
const getPlatformColor = (platform: string) => {
31
+
const colors: Record<string, string> = {
32
+
tiktok: "from-black via-gray-800 to-cyan-400",
33
+
twitter: "from-blue-400 to-blue-600",
34
+
instagram: "from-pink-500 via-purple-500 to-orange-500",
35
+
};
36
+
return colors[platform] || "from-gray-400 to-gray-600";
37
+
};
38
+
39
+
return (
40
+
<div className="p-6">
41
+
<div className="flex items-center space-x-3 mb-3">
42
+
<div>
43
+
<h2 className="text-xl font-bold text-purple-950 dark:text-cyan-50">
44
+
Previously Uploaded
45
+
</h2>
46
+
<p className="text-sm text-purple-750 dark:text-cyan-250">
47
+
Reconnect with your light trail
48
+
</p>
49
+
</div>
50
+
</div>
51
+
52
+
{isLoading ? (
53
+
<div className="space-y-3">
54
+
{[...Array(3)].map((_, i) => (
55
+
<div
56
+
key={i}
57
+
className="animate-pulse flex items-center space-x-4 p-4 bg-purple-100/50 dark:bg-slate-900/50 rounded-xl border-2 border-purple-500/30 dark:border-cyan-500/30"
58
+
>
59
+
<div className="w-12 h-12 bg-purple-200 dark:bg-slate-600 rounded-xl" />
60
+
<div className="flex-1 space-y-2">
61
+
<div className="h-4 bg-purple-200 dark:bg-slate-600 rounded w-3/4" />
62
+
<div className="h-3 bg-purple-200 dark:bg-slate-600 rounded w-1/2" />
63
+
</div>
64
+
</div>
65
+
))}
66
+
</div>
67
+
) : uploads.length === 0 ? (
68
+
<div className="text-center py-12">
69
+
<Upload className="w-16 h-16 text-purple-300 dark:text-slate-600 mx-auto mb-4" />
70
+
<p className="text-purple-750 dark:text-cyan-250 font-medium">
71
+
No previous uploads yet
72
+
</p>
73
+
<p className="text-sm text-purple-750/70 dark:text-cyan-250/70 mt-2">
74
+
Upload your first file to get started
75
+
</p>
76
+
</div>
77
+
) : (
78
+
<div className="space-y-3">
79
+
{uploads.map((upload) => {
80
+
const destApp =
81
+
ATPROTO_APPS[
82
+
userSettings.platformDestinations[
83
+
upload.sourcePlatform as keyof typeof userSettings.platformDestinations
84
+
]
85
+
];
86
+
return (
87
+
<button
88
+
key={upload.uploadId}
89
+
onClick={() => onLoadUpload(upload.uploadId)}
90
+
className="w-full flex items-start space-x-4 p-4 bg-purple-100/20 dark:bg-slate-900/50 hover:bg-purple-100/40 dark:hover:bg-slate-900/70 rounded-xl transition-all text-left border-2 border-orange-650/50 dark:border-amber-400/50 hover:border-orange-500 dark:hover:border-amber-400 shadow-md hover:shadow-lg"
91
+
>
92
+
<div
93
+
className={`w-12 h-12 bg-gradient-to-r ${getPlatformColor(upload.sourcePlatform)} rounded-xl flex items-center justify-center flex-shrink-0 shadow-md`}
94
+
>
95
+
<Sparkles className="w-6 h-6 text-white" />
96
+
</div>
97
+
<div className="flex-1 min-w-0">
98
+
<div className="flex flex-wrap items-start justify-between gap-x-4 gap-y-2 mb-1">
99
+
<div className="font-semibold text-purple-950 dark:text-cyan-50 capitalize leading-tight">
100
+
{upload.sourcePlatform}
101
+
</div>
102
+
<div className="flex items-center gap-2 flex-shrink-0">
103
+
<span className="text-sm text-purple-750 dark:text-cyan-250 whitespace-nowrap flex-shrink-0">
104
+
{upload.matchedUsers}{" "}
105
+
{upload.matchedUsers === 1 ? "match" : "matches"}
106
+
</span>
107
+
</div>
108
+
</div>
109
+
{destApp && (
110
+
<a
111
+
href={destApp.link}
112
+
target="_blank"
113
+
rel="noopener noreferrer"
114
+
className="text-sm text-purple-750 dark:text-cyan-250 hover:underline leading-tight"
115
+
>
116
+
{destApp.action} on {destApp.icon} {destApp.name}
117
+
</a>
118
+
)}
119
+
<div className="flex items-center flex-wrap gap-2">
120
+
<span className="text-xs px-2 py-0.5 rounded-full bg-purple-100 dark:bg-slate-900 text-purple-950 dark:text-cyan-50 font-medium">
121
+
{upload.totalUsers}{" "}
122
+
{upload.totalUsers === 1 ? "user found" : "users found"}
123
+
</span>
124
+
<span className="text-xs px-2 py-0.5 rounded-full bg-purple-100 dark:bg-slate-900 text-purple-950 dark:text-cyan-50 font-medium">
125
+
Uploaded {formatDate(upload.createdAt)}
126
+
</span>
127
+
</div>
128
+
</div>
129
+
</button>
130
+
);
131
+
})}
132
+
</div>
133
+
)}
134
+
</div>
135
+
);
136
+
}
+24
src/components/PlaceholderTab.tsx
+24
src/components/PlaceholderTab.tsx
···
1
+
import { LucideIcon } from "lucide-react";
2
+
3
+
interface PlaceholderTabProps {
4
+
icon: LucideIcon;
5
+
title: string;
6
+
message: string;
7
+
}
8
+
9
+
export default function PlaceholderTab({
10
+
icon: Icon,
11
+
title,
12
+
message,
13
+
}: PlaceholderTabProps) {
14
+
return (
15
+
<div className="p-6">
16
+
<div className="flex items-center space-x-3 mb-6">
17
+
<h2 className="text-xl font-bold text-purple-950 dark:text-cyan-50">
18
+
{title}
19
+
</h2>
20
+
</div>
21
+
<p className="text-purple-900 dark:text-cyan-100">{message}</p>
22
+
</div>
23
+
);
24
+
}
+13
-9
src/components/PlatformSelector.tsx
+13
-9
src/components/PlatformSelector.tsx
···
5
5
onPlatformSelect: (platform: string) => void;
6
6
}
7
7
8
-
export default function PlatformSelector({ onPlatformSelect }: PlatformSelectorProps) {
8
+
export default function PlatformSelector({
9
+
onPlatformSelect,
10
+
}: PlatformSelectorProps) {
9
11
return (
10
-
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
12
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
11
13
{Object.entries(PLATFORMS).map(([key, p]) => {
12
14
const PlatformIcon = p.icon;
13
15
const isEnabled = p.enabled;
···
18
20
disabled={!isEnabled}
19
21
className={`relative p-4 rounded-xl border-2 transition-all ${
20
22
isEnabled
21
-
? 'border-gray-200 dark:border-gray-700 hover:border-blue-300 dark:hover:border-blue-600 hover:shadow-lg cursor-pointer'
22
-
: 'border-gray-200 dark:border-gray-800 opacity-50 cursor-not-allowed'
23
+
? "bg-purple-100/20 dark:bg-slate-900/50 hover:bg-purple-100/40 dark:hover:bg-slate-900/70 border-orange-500/50 dark:border-amber-400/50 hover:border-amber-400 dark:hover:border-amber-400/80 hover:shadow-lg cursor-pointer"
24
+
: "border-cyan-500/30 dark:border-purple-500/30 opacity-50 cursor-not-allowed bg-slate-100/30 dark:bg-slate-900/30"
23
25
}`}
24
-
title={isEnabled ? `Upload ${p.name} data` : 'Coming soon'}
26
+
title={isEnabled ? `Upload ${p.name} data` : "Coming soon"}
25
27
>
26
-
<PlatformIcon className={`w-8 h-8 mx-auto mb-2 ${isEnabled ? 'text-gray-700 dark:text-gray-300' : 'text-gray-400 dark:text-gray-700'}`} />
27
-
<div className="text-sm font-medium text-center text-gray-900 dark:text-gray-100">
28
+
<PlatformIcon
29
+
className={`w-8 h-8 mx-auto mb-2 ${isEnabled ? "text-purple-750 dark:text-cyan-250" : "text-purple-750/50 dark:text-cyan-250/50"}`}
30
+
/>
31
+
<div className="text-sm font-medium text-center text-purple-900 dark:text-cyan-100">
28
32
{p.name}
29
33
</div>
30
34
{!isEnabled && (
31
35
<div className="absolute top-2 right-2">
32
-
<span className="text-xs bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400 px-2 py-0.5 rounded-full">
36
+
<span className="text-xs bg-purple-100 dark:bg-cyan-900 text-purple-600 dark:text-cyan-400 px-2 py-0.5 rounded-full">
33
37
Soon
34
38
</span>
35
39
</div>
···
39
43
})}
40
44
</div>
41
45
);
42
-
}
46
+
}
+136
-103
src/components/SearchResultCard.tsx
+136
-103
src/components/SearchResultCard.tsx
···
1
-
import { Video, MessageCircle, Check, UserPlus, ChevronDown } from "lucide-react";
1
+
import {
2
+
Video,
3
+
MessageCircle,
4
+
Check,
5
+
UserPlus,
6
+
ChevronDown,
7
+
UserCheck,
8
+
} from "lucide-react";
2
9
import { PLATFORMS } from "../constants/platforms";
3
-
import type { SearchResult, AtprotoMatch, SourceUser } from '../types';
4
-
10
+
import type { SearchResult } from "../types";
5
11
6
12
interface SearchResultCardProps {
7
13
result: SearchResult;
···
12
18
sourcePlatform: string;
13
19
}
14
20
15
-
export default function SearchResultCard({
16
-
result,
17
-
resultIndex,
18
-
isExpanded,
19
-
onToggleExpand,
21
+
export default function SearchResultCard({
22
+
result,
23
+
resultIndex,
24
+
isExpanded,
25
+
onToggleExpand,
20
26
onToggleMatchSelection,
21
-
sourcePlatform
27
+
sourcePlatform,
22
28
}: SearchResultCardProps) {
23
-
const displayMatches = isExpanded ? result.atprotoMatches : result.atprotoMatches.slice(0, 1);
29
+
const displayMatches = isExpanded
30
+
? result.atprotoMatches
31
+
: result.atprotoMatches.slice(0, 1);
24
32
const hasMoreMatches = result.atprotoMatches.length > 1;
25
33
const platform = PLATFORMS[sourcePlatform] || PLATFORMS.tiktok;
26
34
27
35
return (
28
-
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-sm overflow-hidden">
36
+
<div className="bg-white/50 dark:bg-slate-900/50 rounded-2xl shadow-sm overflow-hidden border-2 border-cyan-500/30 dark:border-purple-500/30">
29
37
{/* Source User */}
30
-
<div className="px-4 py-3 bg-slate-50 dark:bg-slate-900/50 border-b-2 border-slate-200 dark:border-slate-700">
31
-
<div className="flex items-start justify-between gap-2">
38
+
<div className="px-4 py-3 bg-purple-100 dark:bg-slate-900 border-b-2 border-cyan-500/30 dark:border-purple-500/30">
39
+
<div className="flex justify-between gap-2 items-center">
32
40
<div className="flex-1 min-w-0">
33
-
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
34
-
<span className="font-bold text-slate-900 dark:text-slate-100 truncate text-base">
41
+
<div className="flex flex-wrap gap-x-2 gap-y-1">
42
+
<span className="font-bold text-purple-950 dark:text-cyan-50 truncate text-base">
35
43
@{result.sourceUser.username}
36
44
</span>
37
-
<span className="text-sm text-slate-700 dark:text-slate-300 whitespace-nowrap">
38
-
from {platform.name}
39
-
</span>
40
45
</div>
41
46
</div>
42
-
<div className={`text-xs px-2 py-1 rounded-full bg-indigo-700 dark:bg-pink-700/70 text-white whitespace-nowrap flex-shrink-0`}>
43
-
{result.atprotoMatches.length} {result.atprotoMatches.length === 1 ? 'match' : 'matches'}
47
+
<div
48
+
className={`text-sm text-purple-750 dark:text-cyan-250 whitespace-nowrap flex-shrink-0`}
49
+
>
50
+
{result.atprotoMatches.length}{" "}
51
+
{result.atprotoMatches.length === 1 ? "match" : "matches"}
44
52
</div>
45
53
</div>
46
54
</div>
47
55
48
56
{/* ATProto Matches */}
49
-
<div className="p-4">
50
-
{result.atprotoMatches.length === 0 ? (
51
-
<div className="text-center py-6 text-gray-500 dark:text-gray-400">
52
-
<MessageCircle className="w-8 h-8 mx-auto mb-2 opacity-50" />
53
-
<p className="text-sm">Not found on the ATmosphere yet</p>
54
-
</div>
55
-
) : (
56
-
<div className="space-y-3">
57
-
{displayMatches.map((match) => {
58
-
const isFollowed = match.followed;
59
-
const isSelected = result.selectedMatches?.has(match.did);
60
-
return (
61
-
<div
62
-
key={match.did}
63
-
className="flex items-start space-x-3 p-3 rounded-xl bg-blue-50 dark:bg-blue-900/20 hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-all"
64
-
>
65
-
{/* Avatar */}
66
-
{match.avatar ? (
67
-
<img
68
-
src={match.avatar}
69
-
alt="User avatar"
70
-
className="w-12 h-12 rounded-full object-cover flex-shrink-0"
71
-
/>
72
-
) : (
73
-
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center flex-shrink-0">
74
-
<span className="text-white font-bold">
75
-
{match.handle.charAt(0).toUpperCase()}
76
-
</span>
77
-
</div>
78
-
)}
57
+
{result.atprotoMatches.length === 0 ? (
58
+
<div className="text-center py-6">
59
+
<MessageCircle className="w-8 h-8 mx-auto mb-2 opacity-50 text-purple-750 dark:text-cyan-250" />
60
+
<p className="text-sm text-purple-950 dark:text-cyan-50">
61
+
Not found on the ATmosphere yet
62
+
</p>
63
+
</div>
64
+
) : (
65
+
<div className="">
66
+
{displayMatches.map((match) => {
67
+
const isFollowed = match.followed;
68
+
const isSelected = result.selectedMatches?.has(match.did);
69
+
return (
70
+
<div
71
+
key={match.did}
72
+
className="flex items-start gap-3 p-3 cursor-pointer hover:scale-[1.01] transition-transform"
73
+
>
74
+
{/* Avatar */}
75
+
{match.avatar ? (
76
+
<img
77
+
src={match.avatar}
78
+
alt="User avatar"
79
+
className="w-12 h-12 rounded-full object-cover flex-shrink-0"
80
+
/>
81
+
) : (
82
+
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-cyan-400 to-purple-500 flex items-center justify-center flex-shrink-0">
83
+
<span className="text-white font-bold">
84
+
{match.handle.charAt(0).toUpperCase()}
85
+
</span>
86
+
</div>
87
+
)}
79
88
80
-
{/* Match Info */}
81
-
<div className="flex-1 min-w-0">
89
+
{/* Match Info */}
90
+
<div className="flex-1 min-w-0 space-y-1">
91
+
{/* Name and Handle */}
92
+
<div>
82
93
{match.displayName && (
83
-
<div className="font-semibold text-gray-900 dark:text-gray-100">
94
+
<div className="font-semibold text-purple-950 dark:text-cyan-50 leading-tight">
84
95
{match.displayName}
85
96
</div>
86
97
)}
87
-
<a
98
+
<a
88
99
href={`https://bsky.app/profile/${match.handle}`}
89
100
target="_blank"
90
101
rel="noopener noreferrer"
91
-
className="text-sm text-blue-600 dark:text-blue-400 hover:underline"
102
+
className="text-sm text-purple-750 dark:text-cyan-250 hover:underline leading-tight"
92
103
>
93
104
@{match.handle}
94
105
</a>
95
-
{match.description && (
96
-
<div className="text-sm text-gray-700 dark:text-gray-300 mt-1 line-clamp-2">{match.description}</div>
106
+
</div>
107
+
108
+
{/* User Stats and Match Percent */}
109
+
<div className="flex items-center flex-wrap gap-2">
110
+
{match.postCount && match.postCount > 0 && (
111
+
<span className="text-xs px-2 py-0.5 rounded-full bg-purple-100 dark:bg-slate-900 text-purple-950 dark:text-cyan-50 font-medium">
112
+
{match.postCount.toLocaleString()} posts
113
+
</span>
97
114
)}
98
-
<div className="flex items-center flex-wrap gap-x-3 gap-y-1 mt-2 text-xs text-gray-700 dark:text-gray-300">
99
-
{match.postCount && match.postCount > 0 && (
100
-
<span>{match.postCount.toLocaleString()} posts</span>
101
-
)}
102
-
{match.followerCount && match.followerCount > 0 && (
103
-
<span>{match.followerCount.toLocaleString()} followers</span>
104
-
)}
105
-
<span className="bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-300 px-2 py-1 rounded-full font-medium">
106
-
{match.matchScore}% match
115
+
{match.followerCount && match.followerCount > 0 && (
116
+
<span className="text-xs px-2 py-0.5 rounded-full bg-purple-100 dark:bg-slate-900 text-purple-950 dark:text-cyan-50 font-medium">
117
+
{match.followerCount.toLocaleString()} followers
107
118
</span>
108
-
</div>
119
+
)}
120
+
<span className="text-xs px-2 py-0.5 rounded-full bg-purple-100 dark:bg-slate-900 text-purple-950 dark:text-cyan-50 font-medium">
121
+
{match.matchScore}% match
122
+
</span>
109
123
</div>
110
124
111
-
{/* Select/Follow Button */}
112
-
<button
113
-
onClick={() => onToggleMatchSelection(match.did)}
114
-
disabled={isFollowed}
115
-
className={`p-2 rounded-full font-medium transition-all flex-shrink-0 ${
116
-
isFollowed
117
-
? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 cursor-not-allowed opacity-60'
118
-
: isSelected
119
-
? 'bg-cyan-500 dark:bg-cyan-300 text-white dark:text-slate-700 shadow-md'
120
-
: 'bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-300 dark:hover:bg-slate-600'
121
-
}`}
122
-
title={isFollowed ? 'Already followed' : isSelected ? 'Selected to follow' : 'Select to follow'}
123
-
>
124
-
{isFollowed ? (
125
-
<Check className="w-4 h-4" />
126
-
) : isSelected ? (
127
-
<Check className="w-4 h-4" />
128
-
) : (
129
-
<UserPlus className="w-4 h-4" />
130
-
)}
131
-
</button>
125
+
{/* Description */}
126
+
{match.description && (
127
+
<div className="text-sm text-purple-900 dark:text-cyan-100 line-clamp-2 pt-1">
128
+
{match.description}
129
+
</div>
130
+
)}
132
131
</div>
133
-
);
134
-
})}
135
-
{hasMoreMatches && (
136
-
<button
137
-
onClick={onToggleExpand}
138
-
className="w-full py-2 text-sm text-cyan-700 hover:text-cyan-900 dark:text-cyan-400 dark:hover:text-cyan-200 font-medium transition-colors flex items-center justify-center space-x-1"
139
-
>
140
-
<span>{isExpanded ? 'Show less' : `Show ${result.atprotoMatches.length - 1} more ${result.atprotoMatches.length - 1 === 1 ? 'option' : 'options'}`}</span>
141
-
<ChevronDown className={`w-4 h-4 transition-transform ${isExpanded ? 'rotate-180' : ''}`} />
142
-
</button>
143
-
)}
144
-
</div>
145
-
)}
146
-
</div>
132
+
133
+
{/* Select/Follow Button */}
134
+
<button
135
+
onClick={() => onToggleMatchSelection(match.did)}
136
+
disabled={isFollowed}
137
+
className={`p-2 rounded-full font-medium transition-all flex-shrink-0 self-start ${
138
+
isFollowed
139
+
? "bg-purple-100 dark:bg-slate-900 border-2 border-purple-500 dark:border-cyan-500 text-purple-950 dark:text-cyan-50 shadow-md cursor-not-allowed opacity-60"
140
+
: isSelected
141
+
? "bg-purple-100 dark:bg-slate-900 border-2 border-purple-500 dark:border-cyan-500 text-purple-950 dark:text-cyan-50 shadow-md"
142
+
: "bg-slate-200/50 dark:bg-slate-900/50 border-2 border-cyan-500/30 dark:border-purple-500/30 text-purple-750 dark:text-cyan-250 hover:border-orange-500 dark:hover:border-amber-400"
143
+
}`}
144
+
title={
145
+
isFollowed
146
+
? "Already followed"
147
+
: isSelected
148
+
? "Selected to follow"
149
+
: "Select to follow"
150
+
}
151
+
>
152
+
{isFollowed ? (
153
+
<Check className="w-4 h-4" />
154
+
) : isSelected ? (
155
+
<UserCheck className="w-4 h-4" />
156
+
) : (
157
+
<UserPlus className="w-4 h-4" />
158
+
)}
159
+
</button>
160
+
</div>
161
+
);
162
+
})}
163
+
{hasMoreMatches && (
164
+
<button
165
+
onClick={onToggleExpand}
166
+
className="w-full py-2 text-sm text-purple-600 hover:text-purple-950 dark:text-cyan-400 dark:hover:text-cyan-50 font-medium transition-colors flex items-center justify-center space-x-1 border-t-2 border-cyan-500/30 dark:border-purple-500/30 hover:border-orange-500 dark:hover:border-amber-400/50"
167
+
>
168
+
<span>
169
+
{isExpanded
170
+
? "Show less"
171
+
: `Show ${result.atprotoMatches.length - 1} more ${result.atprotoMatches.length - 1 === 1 ? "option" : "options"}`}
172
+
</span>
173
+
<ChevronDown
174
+
className={`w-4 h-4 transition-transform ${isExpanded ? "rotate-180" : ""}`}
175
+
/>
176
+
</button>
177
+
)}
178
+
</div>
179
+
)}
147
180
</div>
148
181
);
149
-
}
182
+
}
+201
-127
src/components/SetupWizard.tsx
+201
-127
src/components/SetupWizard.tsx
···
1
-
import { useState } from 'react';
2
-
import { Heart, X, Check, ChevronRight } from 'lucide-react';
3
-
import { PLATFORMS } from '../constants/platforms';
4
-
import { ATPROTO_APPS } from '../constants/atprotoApps';
5
-
import type { UserSettings, PlatformDestinations } from '../types/settings';
1
+
import { useState } from "react";
2
+
import { Heart, X, Check, ChevronRight } from "lucide-react";
3
+
import { PLATFORMS } from "../constants/platforms";
4
+
import { ATPROTO_APPS } from "../constants/atprotoApps";
5
+
import type { UserSettings, PlatformDestinations } from "../types/settings";
6
6
7
7
interface SetupWizardProps {
8
8
isOpen: boolean;
···
12
12
}
13
13
14
14
const wizardSteps = [
15
-
{ title: 'Welcome', description: 'Set up your preferences' },
16
-
{ title: 'Platforms', description: 'Choose where to import from' },
17
-
{ title: 'Destinations', description: 'Where should matches go?' },
18
-
{ title: 'Privacy', description: 'Data & automation settings' },
19
-
{ title: 'Ready!', description: 'All set to find your people' },
15
+
{ title: "Welcome", description: "Set up your preferences" },
16
+
{ title: "Platforms", description: "Choose where to import from" },
17
+
{ title: "Destinations", description: "Where should matches go?" },
18
+
{ title: "Privacy", description: "Data & automation settings" },
19
+
{ title: "Ready!", description: "All set to find your people" },
20
20
];
21
21
22
-
export default function SetupWizard({ isOpen, onClose, onComplete, currentSettings }: SetupWizardProps) {
22
+
export default function SetupWizard({
23
+
isOpen,
24
+
onClose,
25
+
onComplete,
26
+
currentSettings,
27
+
}: SetupWizardProps) {
23
28
const [wizardStep, setWizardStep] = useState(0);
24
-
const [selectedPlatforms, setSelectedPlatforms] = useState<Set<string>>(new Set());
25
-
const [platformDestinations, setPlatformDestinations] = useState<PlatformDestinations>(
26
-
currentSettings.platformDestinations
29
+
const [selectedPlatforms, setSelectedPlatforms] = useState<Set<string>>(
30
+
new Set(),
27
31
);
32
+
const [platformDestinations, setPlatformDestinations] =
33
+
useState<PlatformDestinations>(currentSettings.platformDestinations);
28
34
const [saveData, setSaveData] = useState(currentSettings.saveData);
29
-
const [enableAutomation, setEnableAutomation] = useState(currentSettings.enableAutomation);
30
-
const [automationFrequency, setAutomationFrequency] = useState(currentSettings.automationFrequency);
35
+
const [enableAutomation, setEnableAutomation] = useState(
36
+
currentSettings.enableAutomation,
37
+
);
38
+
const [automationFrequency, setAutomationFrequency] = useState(
39
+
currentSettings.automationFrequency,
40
+
);
31
41
32
42
if (!isOpen) return null;
33
43
···
53
63
};
54
64
55
65
// Get platforms to show on destinations page (only selected ones)
56
-
const platformsToShow = selectedPlatforms.size > 0
57
-
? Object.entries(PLATFORMS).filter(([key]) => selectedPlatforms.has(key))
58
-
: Object.entries(PLATFORMS);
66
+
const platformsToShow =
67
+
selectedPlatforms.size > 0
68
+
? Object.entries(PLATFORMS).filter(([key]) => selectedPlatforms.has(key))
69
+
: Object.entries(PLATFORMS);
59
70
60
71
return (
61
72
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
62
-
<div className="bg-white dark:bg-gray-800 rounded-2xl max-w-2xl w-full shadow-2xl max-h-[90vh] flex flex-col">
73
+
<div className="bg-white dark:bg-slate-900 backdrop-blur-xl rounded-2xl max-w-2xl w-full shadow-2xl max-h-[90vh] flex flex-col border-2 border-cyan-500/30 dark:border-purple-500/30">
63
74
{/* Header */}
64
-
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
65
-
<div className="flex items-center justify-between mb-4">
75
+
<div className="px-6 py-4 border-b-2 border-cyan-500/30 dark:border-purple-500/30 flex-shrink-0">
76
+
<div className="flex items-center justify-between mb-3">
66
77
<div className="flex items-center space-x-3">
67
78
<div className="w-10 h-10 bg-gradient-to-br from-firefly-amber via-firefly-orange to-firefly-pink rounded-xl flex items-center justify-center shadow-md">
68
79
<Heart className="w-5 h-5 text-white" />
69
80
</div>
70
-
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Setup Assistant</h2>
81
+
<h2 className="text-2xl font-bold text-purple-950 dark:text-cyan-50">
82
+
Setup Assistant
83
+
</h2>
71
84
</div>
72
-
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
85
+
<button
86
+
onClick={onClose}
87
+
className="text-purple-600 dark:text-cyan-400 hover:text-purple-950 dark:hover:text-cyan-50 transition-colors"
88
+
>
73
89
<X className="w-6 h-6" />
74
90
</button>
75
91
</div>
···
79
95
<div key={idx} className="flex-1">
80
96
<div
81
97
className={`h-2 rounded-full transition-all ${
82
-
idx <= wizardStep ? 'bg-gradient-to-r from-firefly-cyan via-firefly-orange to-firefly-pink' : 'bg-gray-200 dark:bg-gray-700'
98
+
idx <= wizardStep
99
+
? "bg-orange-500"
100
+
: "bg-cyan-500/30 dark:bg-purple-500/30"
83
101
}`}
84
102
/>
85
103
</div>
86
104
))}
87
105
</div>
88
-
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
89
-
Step {wizardStep + 1} of {wizardSteps.length}: {wizardSteps[wizardStep].title}
106
+
<div className="mt-2 text-sm text-purple-750 dark:text-cyan-250">
107
+
Step {wizardStep + 1} of {wizardSteps.length}:{" "}
108
+
{wizardSteps[wizardStep].title}
90
109
</div>
91
110
</div>
92
111
93
112
{/* Content - Scrollable */}
94
-
<div className="p-6 overflow-y-auto flex-1">
113
+
<div className="px-6 py-4 overflow-y-auto flex-1">
95
114
{wizardStep === 0 && (
96
-
<div className="text-center space-y-4">
97
-
<div className="text-6xl mb-4">👋</div>
98
-
<h3 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Welcome to ATlast!</h3>
99
-
<p className="text-gray-600 dark:text-gray-400 max-w-md mx-auto">
100
-
Let's get you set up in just a few steps. We'll help you configure how you want to reconnect with your
101
-
community on the ATmosphere.
115
+
<div className="text-center space-y-3">
116
+
<div className="text-6xl mb-2">👋</div>
117
+
<h3 className="text-2xl font-bold text-purple-950 dark:text-cyan-50">
118
+
Welcome to ATlast!
119
+
</h3>
120
+
<p className="text-purple-750 dark:text-cyan-250 max-w-md mx-auto">
121
+
Let's get you set up in just a few steps. We'll help you
122
+
configure how you want to reconnect with your community on the
123
+
ATmosphere.
102
124
</p>
103
125
</div>
104
126
)}
105
127
106
128
{wizardStep === 1 && (
107
-
<div className="space-y-4">
108
-
<h3 className="text-xl font-bold text-gray-900 dark:text-gray-100">Which platforms will you import from?</h3>
109
-
<p className="text-sm text-gray-600 dark:text-gray-400">
110
-
Select one or more platforms you follow people on. We'll help you find them on the ATmosphere.
129
+
<div className="space-y-3">
130
+
<h3 className="text-xl font-bold text-purple-950 dark:text-cyan-50">
131
+
Which platforms will you import from?
132
+
</h3>
133
+
<p className="text-sm text-purple-750 dark:text-cyan-250">
134
+
Select one or more platforms you follow people on. We'll help
135
+
you find them on the ATmosphere.
111
136
</p>
112
-
<div className="grid grid-cols-3 gap-3 mt-4">
137
+
<div className="grid grid-cols-3 gap-3 mt-3">
113
138
{Object.entries(PLATFORMS).map(([key, p]) => {
114
139
const Icon = p.icon;
115
140
const isSelected = selectedPlatforms.has(key);
···
119
144
onClick={() => togglePlatform(key)}
120
145
className={`p-4 rounded-xl border-2 transition-all relative ${
121
146
isSelected
122
-
? 'border-firefly-orange bg-firefly-orange/10 dark:bg-firefly-orange/20'
123
-
: 'border-gray-200 dark:border-gray-700 hover:border-firefly-cyan'
147
+
? "bg-purple-100/50 dark:bg-slate-950/50 border-2 border-purple-500 dark:border-cyan-500 text-purple-950 dark:text-cyan-50"
148
+
: "border-cyan-500/30 dark:border-purple-500/30 hover:bg-purple-100/50 dark:hover:bg-slate-950/50 hover:border-orange-500 dark:hover:border-amber-400"
124
149
}`}
125
150
>
126
151
{isSelected && (
127
-
<div className="absolute -top-2 -right-2 w-6 h-6 bg-firefly-orange rounded-full flex items-center justify-center">
128
-
<Check className="w-4 h-4 text-white" />
152
+
<div className="absolute -top-2 -right-2 w-6 h-6 bg-orange-500 dark:bg-amber-400 rounded-full flex items-center justify-center shadow-md">
153
+
<Check className="w-4 h-4 text-white dark:text-slate-900" />
129
154
</div>
130
155
)}
131
-
<Icon className="w-8 h-8 mx-auto mb-2 text-gray-700 dark:text-gray-300" />
132
-
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">{p.name}</div>
156
+
<Icon className="w-8 h-8 mx-auto mb-2 text-purple-750 dark:text-cyan-250" />
157
+
<div className="text-sm font-medium text-purple-950 dark:text-cyan-50">
158
+
{p.name}
159
+
</div>
133
160
</button>
134
161
);
135
162
})}
136
163
</div>
137
164
{selectedPlatforms.size > 0 && (
138
-
<div className="mt-4 p-3 bg-firefly-amber/10 dark:bg-firefly-amber/20 rounded-lg border border-firefly-amber/30">
139
-
<p className="text-sm text-gray-700 dark:text-gray-300">
140
-
✨ {selectedPlatforms.size} platform{selectedPlatforms.size !== 1 ? 's' : ''} selected
165
+
<div className="mt-3 px-3 py-2 rounded-lg border border-orange-650/30 dark:border-amber-400/30">
166
+
<p className="text-sm text-purple-750 dark:text-cyan-250">
167
+
✨ {selectedPlatforms.size} platform
168
+
{selectedPlatforms.size !== 1 ? "s" : ""} selected
141
169
</p>
142
170
</div>
143
171
)}
···
146
174
147
175
{wizardStep === 2 && (
148
176
<div className="space-y-4">
149
-
<h3 className="text-xl font-bold text-gray-900 dark:text-gray-100">Where should matches go?</h3>
150
-
<p className="text-sm text-gray-600 dark:text-gray-400">
151
-
Choose which ATmosphere app to use for each platform. You can change this later.
177
+
<h3 className="text-xl font-bold text-purple-950 dark:text-cyan-50">
178
+
Where should matches go?
179
+
</h3>
180
+
<p className="text-sm text-purple-750 dark:text-cyan-250">
181
+
Choose which ATmosphere app to use for each platform. You can
182
+
change this later.
152
183
</p>
153
-
<div className="space-y-3 mt-4">
184
+
<div className="space-y-4 mt-3">
154
185
{platformsToShow.map(([key, p]) => {
155
186
const Icon = p.icon;
156
187
return (
157
-
<div key={key} className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-900 rounded-xl">
158
-
<div className="flex items-center space-x-3">
159
-
<Icon className="w-6 h-6 text-gray-700 dark:text-gray-300" />
160
-
<span className="font-medium text-gray-900 dark:text-gray-100">{p.name}</span>
188
+
<div
189
+
key={key}
190
+
className="flex items-center px-3 max-w-lg mx-sm border-cyan-500/30 dark:border-purple-500/30"
191
+
>
192
+
<div className="flex space-x-3">
193
+
<Icon className="w-6 h-6 text-purple-950 dark:text-cyan-50" />
194
+
<span className="font-medium text-purple-950 dark:text-cyan-50">
195
+
{p.name}
196
+
</span>
161
197
</div>
162
198
<select
163
-
value={platformDestinations[key as keyof PlatformDestinations]}
199
+
value={
200
+
platformDestinations[
201
+
key as keyof PlatformDestinations
202
+
]
203
+
}
164
204
onChange={(e) =>
165
205
setPlatformDestinations({
166
206
...platformDestinations,
167
207
[key]: e.target.value,
168
208
})
169
209
}
170
-
className="px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-sm text-gray-900 dark:text-gray-100"
210
+
className="px-3 py-2 ml-auto bg-white dark:bg-slate-800 border border-cyan-500/30 dark:border-purple-500/30 rounded-lg text-sm text-purple-950 dark:text-cyan-50 hover:border-cyan-400 dark:hover:border-purple-400 focus:outline-none focus:ring-2 focus:ring-orange-500 dark:focus:ring-amber-400 transition-colors"
171
211
>
172
212
{Object.values(ATPROTO_APPS).map((app) => (
173
213
<option key={app.id} value={app.id}>
···
183
223
)}
184
224
185
225
{wizardStep === 3 && (
186
-
<div className="space-y-4">
226
+
<div className="space-y-3">
187
227
<div>
188
-
<h3 className="text-xl font-bold text-gray-900 dark:text-gray-100 mb-2">Privacy & Automation</h3>
189
-
<p className="text-sm text-gray-600 dark:text-gray-400">Control how your data is used.</p>
228
+
<h3 className="text-xl font-bold text-purple-950 dark:text-cyan-50 mb-1">
229
+
Privacy & Automation
230
+
</h3>
231
+
<p className="text-sm text-purple-750 dark:text-cyan-250">
232
+
Control how your data is used.
233
+
</p>
190
234
</div>
191
235
192
236
<div className="space-y-3">
193
-
<div className="p-4 bg-firefly-cyan/10 dark:bg-firefly-cyan/20 rounded-xl border border-firefly-cyan/30">
194
-
<div className="flex items-start space-x-3">
195
-
<input
196
-
type="checkbox"
197
-
checked={saveData}
198
-
onChange={(e) => setSaveData(e.target.checked)}
199
-
className="mt-1"
200
-
id="save-data"
201
-
/>
202
-
<div className="flex-1">
203
-
<label htmlFor="save-data" className="font-medium text-gray-900 dark:text-gray-100 cursor-pointer">
204
-
Save my data for future checks
205
-
</label>
206
-
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
207
-
Store your following lists so we can check for new matches later. You can delete anytime.
208
-
</p>
209
-
</div>
237
+
<div className="flex items-start space-x-3 px-4 py-3">
238
+
<input
239
+
type="checkbox"
240
+
checked={saveData}
241
+
onChange={(e) => setSaveData(e.target.checked)}
242
+
className="mt-1"
243
+
id="save-data"
244
+
/>
245
+
<div className="flex-1">
246
+
<label
247
+
htmlFor="save-data"
248
+
className="font-medium text-purple-950 dark:text-cyan-50 cursor-pointer"
249
+
>
250
+
Save my data for future checks
251
+
</label>
252
+
<p className="text-sm text-purple-950 dark:text-cyan-250 mt-1">
253
+
Store your following lists so we can check for new matches
254
+
later. You can delete anytime.
255
+
</p>
210
256
</div>
211
257
</div>
212
258
213
-
<div className="p-4 bg-firefly-pink/10 dark:bg-firefly-pink/20 rounded-xl border border-firefly-pink/30">
214
-
<div className="flex items-start space-x-3">
215
-
<input
216
-
type="checkbox"
217
-
checked={enableAutomation}
218
-
onChange={(e) => setEnableAutomation(e.target.checked)}
219
-
className="mt-1"
220
-
id="enable-automation"
221
-
/>
222
-
<div className="flex-1">
223
-
<label htmlFor="enable-automation" className="font-medium text-gray-900 dark:text-gray-100 cursor-pointer">
224
-
Notify me about new matches
225
-
</label>
226
-
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
227
-
We'll check periodically and DM you when people you follow join the ATmosphere.
228
-
</p>
229
-
{enableAutomation && (
230
-
<select
231
-
value={automationFrequency}
232
-
onChange={(e) => setAutomationFrequency(e.target.value as 'weekly' | 'monthly' | 'quarterly')}
233
-
className="mt-2 px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-sm w-full text-gray-900 dark:text-gray-100"
234
-
>
235
-
<option value="daily">Check daily</option>
236
-
<option value="weekly">Check weekly</option>
237
-
<option value="monthly">Check monthly</option>
238
-
</select>
239
-
)}
240
-
</div>
259
+
<div className="flex items-start space-x-3 px-4 py-3">
260
+
<input
261
+
type="checkbox"
262
+
checked={enableAutomation}
263
+
onChange={(e) => setEnableAutomation(e.target.checked)}
264
+
className="mt-1"
265
+
id="enable-automation"
266
+
/>
267
+
<div className="flex-1">
268
+
<label
269
+
htmlFor="enable-automation"
270
+
className="font-medium text-purple-950 dark:text-cyan-50 cursor-pointer"
271
+
>
272
+
Notify me about new matches
273
+
</label>
274
+
<p className="text-sm text-purple-750 dark:text-cyan-250 mt-1">
275
+
We'll check periodically and DM you when people you follow
276
+
join the ATmosphere.
277
+
</p>
278
+
{enableAutomation && (
279
+
<select
280
+
value={automationFrequency}
281
+
onChange={(e) =>
282
+
setAutomationFrequency(
283
+
e.target.value as
284
+
| "weekly"
285
+
| "monthly"
286
+
| "quarterly",
287
+
)
288
+
}
289
+
className="mt-2 px-3 py-2 bg-white dark:bg-slate-800 border border-cyan-500/30 dark:border-purple-500/30 rounded-lg text-sm w-full text-purple-950 dark:text-cyan-50 hover:border-cyan-400 dark:hover:border-purple-400 focus:outline-none focus:ring-2 focus:ring-orange-500 dark:focus:ring-amber-400 transition-colors"
290
+
>
291
+
<option value="daily">Check daily</option>
292
+
<option value="weekly">Check weekly</option>
293
+
<option value="monthly">Check monthly</option>
294
+
</select>
295
+
)}
241
296
</div>
242
297
</div>
243
298
</div>
···
245
300
)}
246
301
247
302
{wizardStep === 4 && (
248
-
<div className="text-center space-y-4">
249
-
<div className="text-6xl mb-4">🎉</div>
250
-
<h3 className="text-2xl font-bold text-gray-900 dark:text-gray-100">You're all set!</h3>
251
-
<p className="text-gray-600 dark:text-gray-400 max-w-md mx-auto">
252
-
Your preferences have been saved. You can change them anytime in Settings.
303
+
<div className="text-center space-y-3">
304
+
<div className="text-6xl mb-2">🎉</div>
305
+
<h3 className="text-2xl font-bold text-purple-950 dark:text-cyan-50">
306
+
You're all set!
307
+
</h3>
308
+
<p className="text-purple-750 dark:text-cyan-250 max-w-md mx-auto">
309
+
Your preferences have been saved. You can change them anytime in
310
+
Settings.
253
311
</p>
254
-
<div className="bg-gradient-to-r from-firefly-cyan/20 via-firefly-orange/20 to-firefly-pink/20 dark:from-firefly-cyan/10 dark:via-firefly-orange/10 dark:to-firefly-pink/10 rounded-xl p-4 mt-4 border border-firefly-orange/30">
255
-
<h4 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">Quick Summary:</h4>
256
-
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1 text-left max-w-sm mx-auto">
312
+
<div className="px-4 py-3 mt-3">
313
+
<h4 className="font-semibold text-purple-950 dark:text-cyan-50 mb-2">
314
+
Quick Summary:
315
+
</h4>
316
+
<ul className="text-sm text-purple-750 dark:text-cyan-250 space-y-1 text-left max-w-sm mx-auto">
257
317
<li className="flex items-center space-x-2">
258
-
<Check className="w-4 h-4 text-firefly-orange" />
259
-
<span>Data saving: {saveData ? 'Enabled' : 'Disabled'}</span>
318
+
<Check className="w-4 h-4 text-orange-500" />
319
+
<span>
320
+
Data saving: {saveData ? "Enabled" : "Disabled"}
321
+
</span>
260
322
</li>
261
323
<li className="flex items-center space-x-2">
262
-
<Check className="w-4 h-4 text-firefly-orange" />
263
-
<span>Automation: {enableAutomation ? 'Enabled' : 'Disabled'}</span>
324
+
<Check className="w-4 h-4 text-orange-500" />
325
+
<span>
326
+
Automation: {enableAutomation ? "Enabled" : "Disabled"}
327
+
</span>
264
328
</li>
265
329
<li className="flex items-center space-x-2">
266
-
<Check className="w-4 h-4 text-firefly-orange" />
267
-
<span>Platforms: {selectedPlatforms.size > 0 ? selectedPlatforms.size : 'All'} selected</span>
330
+
<Check className="w-4 h-4 text-orange-500" />
331
+
<span>
332
+
Platforms:{" "}
333
+
{selectedPlatforms.size > 0
334
+
? selectedPlatforms.size
335
+
: "All"}{" "}
336
+
selected
337
+
</span>
268
338
</li>
269
339
<li className="flex items-center space-x-2">
270
-
<Check className="w-4 h-4 text-firefly-orange" />
340
+
<Check className="w-4 h-4 text-orange-500" />
271
341
<span>Ready to upload your first file!</span>
272
342
</li>
273
343
</ul>
···
277
347
</div>
278
348
279
349
{/* Footer */}
280
-
<div className="p-6 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between flex-shrink-0">
350
+
<div className="px-6 py-4 border-t-2 border-cyan-500/30 dark:border-purple-500/30 flex items-center justify-between flex-shrink-0">
281
351
<button
282
352
onClick={() => wizardStep > 0 && setWizardStep(wizardStep - 1)}
283
353
disabled={wizardStep === 0}
284
-
className="px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
354
+
className="px-4 py-2 text-purple-750 dark:text-cyan-250 hover:text-purple-950 dark:hover:text-cyan-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
285
355
>
286
356
Back
287
357
</button>
···
293
363
handleComplete();
294
364
}
295
365
}}
296
-
className="px-6 py-2 bg-gradient-to-r from-firefly-amber via-firefly-orange to-firefly-pink text-white rounded-lg font-medium hover:shadow-lg transition-all flex items-center space-x-2"
366
+
className="px-6 py-2 bg-orange-600 hover:bg-orange-500 text-white rounded-lg font-medium shadow-md hover:shadow-lg transition-all flex items-center space-x-2"
297
367
>
298
-
<span>{wizardStep === wizardSteps.length - 1 ? 'Get Started' : 'Next'}</span>
299
-
{wizardStep < wizardSteps.length - 1 && <ChevronRight className="w-4 h-4" />}
368
+
<span>
369
+
{wizardStep === wizardSteps.length - 1 ? "Get Started" : "Next"}
370
+
</span>
371
+
{wizardStep < wizardSteps.length - 1 && (
372
+
<ChevronRight className="w-4 h-4" />
373
+
)}
300
374
</button>
301
375
</div>
302
376
</div>
303
377
</div>
304
378
);
305
-
}
379
+
}
+7
-7
src/components/ThemeControls.tsx
+7
-7
src/components/ThemeControls.tsx
···
1
-
import { Sun, Moon, Pause, Play } from 'lucide-react';
1
+
import { Sun, Moon, Pause, Play } from "lucide-react";
2
2
3
3
interface ThemeControlsProps {
4
4
isDark: boolean;
···
7
7
onToggleMotion: () => void;
8
8
}
9
9
10
-
export default function ThemeControls({
11
-
isDark,
12
-
reducedMotion,
13
-
onToggleTheme,
14
-
onToggleMotion
10
+
export default function ThemeControls({
11
+
isDark,
12
+
reducedMotion,
13
+
onToggleTheme,
14
+
onToggleMotion,
15
15
}: ThemeControlsProps) {
16
16
return (
17
17
<div className="flex items-center space-x-2">
···
40
40
</button>
41
41
</div>
42
42
);
43
-
}
43
+
}
+94
src/components/UploadTab.tsx
+94
src/components/UploadTab.tsx
···
1
+
import { Upload, ChevronRight, Settings } from "lucide-react";
2
+
import { useRef } from "react";
3
+
import PlatformSelector from "../components/PlatformSelector";
4
+
5
+
interface UploadTabProps {
6
+
wizardCompleted: boolean;
7
+
onShowWizard: () => void;
8
+
onPlatformSelect: (platform: string) => void;
9
+
onFileUpload: (
10
+
e: React.ChangeEvent<HTMLInputElement>,
11
+
platform: string,
12
+
) => void;
13
+
selectedPlatform: string;
14
+
}
15
+
16
+
export default function UploadTab({
17
+
wizardCompleted,
18
+
onShowWizard,
19
+
onPlatformSelect,
20
+
onFileUpload,
21
+
selectedPlatform,
22
+
}: UploadTabProps) {
23
+
const fileInputRef = useRef<HTMLInputElement>(null);
24
+
25
+
const handlePlatformSelect = (platform: string) => {
26
+
onPlatformSelect(platform);
27
+
fileInputRef.current?.click();
28
+
};
29
+
30
+
return (
31
+
<div className="p-6">
32
+
{/* Setup Assistant Banner - Only show if wizard not completed */}
33
+
{!wizardCompleted && (
34
+
<div className="bg-firefly-banner dark:bg-firefly-banner-dark rounded-2xl p-6 text-white">
35
+
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
36
+
<div className="flex-1">
37
+
<h2 className="text-2xl font-bold mb-2">
38
+
Need help getting started?
39
+
</h2>
40
+
<p className="text-white/90">
41
+
Run the setup assistant to configure your preferences in
42
+
minutes.
43
+
</p>
44
+
</div>
45
+
<button
46
+
onClick={onShowWizard}
47
+
className="bg-white text-slate-900 px-6 py-3 rounded-xl font-semibold hover:bg-slate-100 transition-all flex items-center space-x-2 whitespace-nowrap shadow-lg"
48
+
>
49
+
<span>Start Setup</span>
50
+
<ChevronRight className="w-4 h-4" />
51
+
</button>
52
+
</div>
53
+
</div>
54
+
)}
55
+
56
+
{/* Upload Section */}
57
+
<div className="space-y-3">
58
+
<div className="flex items-center justify-between mb-4">
59
+
<div className="flex items-center space-x-3">
60
+
<div>
61
+
<h2 className="text-xl font-bold text-purple-950 dark:text-cyan-50">
62
+
Upload Following Data
63
+
</h2>
64
+
<p className="text-sm text-purple-750 dark:text-cyan-250">
65
+
Find your people on the ATmosphere
66
+
</p>
67
+
</div>
68
+
</div>
69
+
{wizardCompleted && (
70
+
<button
71
+
onClick={onShowWizard}
72
+
className="text-sm text-orange-650 hover:text-orange-500 dark:text-amber-400 dark:hover:text-amber-300 font-medium transition-colors flex items-center space-x-1"
73
+
>
74
+
<Settings className="w-4 h-4" />
75
+
<span>Reconfigure</span>
76
+
</button>
77
+
)}
78
+
</div>
79
+
80
+
<PlatformSelector onPlatformSelect={handlePlatformSelect} />
81
+
82
+
<input
83
+
id="file-upload"
84
+
ref={fileInputRef}
85
+
type="file"
86
+
accept=".txt,.json,.html,.zip"
87
+
onChange={(e) => onFileUpload(e, selectedPlatform || "tiktok")}
88
+
className="sr-only"
89
+
aria-label="Upload following data file"
90
+
/>
91
+
</div>
92
+
</div>
93
+
);
94
+
}
+27
-27
src/constants/atprotoApps.ts
+27
-27
src/constants/atprotoApps.ts
···
1
-
import type { AtprotoApp } from '../types/settings';
1
+
import type { AtprotoApp } from "../types/settings";
2
2
3
3
export const ATPROTO_APPS: Record<string, AtprotoApp> = {
4
4
bluesky: {
5
-
id: 'bluesky',
6
-
name: 'Bluesky',
7
-
description: 'The main ATmosphere social network',
8
-
color: 'blue',
9
-
icon: '🦋',
10
-
action: 'Follow',
5
+
id: "bluesky",
6
+
name: "Bluesky",
7
+
description: "The main ATmosphere social network",
8
+
link: "https://bsky.app/",
9
+
icon: "🦋",
10
+
action: "Follow",
11
11
enabled: true,
12
12
},
13
13
tangled: {
14
-
id: 'tangled',
15
-
name: 'Tangled',
16
-
description: 'Alternative following for developers & creators',
17
-
color: 'purple',
18
-
icon: '🐑',
19
-
action: 'Follow',
14
+
id: "tangled",
15
+
name: "Tangled",
16
+
description: "Alternative following for developers & creators",
17
+
link: "https://tangled.org/",
18
+
icon: "🐑",
19
+
action: "Follow",
20
20
enabled: false, // Not yet integrated
21
21
},
22
22
spark: {
23
-
id: 'spark',
24
-
name: 'Spark',
25
-
description: 'Short-form video focused social',
26
-
color: 'orange',
27
-
icon: '✨',
28
-
action: 'Follow',
23
+
id: "spark",
24
+
name: "Spark",
25
+
description: "Short-form video focused social",
26
+
link: "https://sprk.so/",
27
+
icon: "✨",
28
+
action: "Follow",
29
29
enabled: false, // Not yet integrated
30
30
},
31
31
lists: {
32
-
id: 'bsky list',
33
-
name: 'List',
34
-
description: 'Organize into custom Bluesky lists',
35
-
color: 'green',
36
-
icon: '📃',
37
-
action: 'Add to List',
32
+
id: "bsky list",
33
+
name: "Bluesky List",
34
+
description: "Organize into custom Bluesky lists",
35
+
link: "https://bsky.app/",
36
+
icon: "📃",
37
+
action: "Add to",
38
38
enabled: false, // Not yet implemented
39
39
},
40
40
};
···
44
44
}
45
45
46
46
export function getEnabledApps(): AtprotoApp[] {
47
-
return Object.values(ATPROTO_APPS).filter(app => app.enabled);
48
-
}
47
+
return Object.values(ATPROTO_APPS).filter((app) => app.enabled);
48
+
}
+21
-5
src/index.css
+21
-5
src/index.css
···
1
+
@import url("https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;500;600;700;800&family=Rubik:wght@400;500;600;700&display=swap");
1
2
@tailwind base;
2
3
@tailwind components;
3
4
@tailwind utilities;
4
5
5
6
@layer base {
7
+
* {
8
+
font-family:
9
+
"Fira Sans",
10
+
system-ui,
11
+
-apple-system,
12
+
sans-serif;
13
+
}
14
+
15
+
h1,
16
+
h2,
17
+
h3,
18
+
.font-display {
19
+
font-family: "Rubik", "Fira Sans", sans-serif;
20
+
}
21
+
6
22
body {
7
-
font-family: system-ui, sans-serif;
8
-
@apply bg-gradient-to-br from-amber-50 via-orange-50 to-pink-50
9
-
dark:from-indigo-950 dark:via-purple-900 dark:to-slate-900
10
-
text-slate-900 dark:text-slate-100
11
-
transition-colors duration-300;
23
+
@apply bg-gradient-to-br
24
+
from-cyan-50 via-purple-50 to-pink-50
25
+
dark:from-indigo-950 dark:via-purple-900 dark:to-slate-900
26
+
text-slate-900 dark:text-slate-100
27
+
transition-colors duration-300;
12
28
}
13
29
14
30
button {
+49
-267
src/pages/Home.tsx
+49
-267
src/pages/Home.tsx
···
1
-
import {
2
-
Upload,
3
-
History,
4
-
Settings,
5
-
BookOpen,
6
-
Grid3x3,
7
-
ChevronRight,
8
-
Sparkles,
9
-
} from "lucide-react";
10
-
import { useState, useEffect, useRef } from "react";
1
+
import { BookOpen, Grid3x3 } from "lucide-react";
2
+
import { useState, useEffect } from "react";
11
3
import AppHeader from "../components/AppHeader";
12
-
import PlatformSelector from "../components/PlatformSelector";
13
4
import SetupWizard from "../components/SetupWizard";
5
+
import TabNavigation, { TabId } from "../components/TabNavigation";
6
+
import UploadTab from "../components/UploadTab";
7
+
import HistoryTab from "../components/HistoryTab";
8
+
import PlaceholderTab from "../components/PlaceholderTab";
14
9
import { apiClient } from "../lib/apiClient";
15
-
import { ATPROTO_APPS } from "../constants/atprotoApps";
16
10
import type { Upload as UploadType } from "../types";
17
11
import type { UserSettings } from "../types/settings";
18
12
import SettingsPage from "./Settings";
···
37
31
currentStep: string;
38
32
userSettings: UserSettings;
39
33
onSettingsUpdate: (settings: Partial<UserSettings>) => void;
40
-
// New props from changes.js
41
34
reducedMotion?: boolean;
42
35
isDark?: boolean;
43
36
onToggleTheme?: () => void;
44
37
onToggleMotion?: () => void;
45
38
}
46
39
47
-
type TabId = "upload" | "history" | "settings" | "guides" | "apps";
48
-
49
40
export default function HomePage({
50
41
session,
51
42
onLogout,
···
65
56
const [isLoading, setIsLoading] = useState(true);
66
57
const [selectedPlatform, setSelectedPlatform] = useState<string>("");
67
58
const [showWizard, setShowWizard] = useState(false);
68
-
const fileInputRef = useRef<HTMLInputElement>(null);
69
59
70
60
useEffect(() => {
71
61
if (session) {
···
90
80
}
91
81
}
92
82
93
-
const handlePlatformSelect = (platform: string) => {
94
-
setSelectedPlatform(platform);
95
-
fileInputRef.current?.click();
96
-
};
97
-
98
-
const formatDate = (dateString: string) => {
99
-
const date = new Date(dateString);
100
-
return date.toLocaleDateString("en-US", {
101
-
month: "short",
102
-
day: "numeric",
103
-
year: "numeric",
104
-
hour: "2-digit",
105
-
minute: "2-digit",
106
-
});
107
-
};
108
-
109
-
const getPlatformColor = (platform: string) => {
110
-
const colors: Record<string, string> = {
111
-
tiktok: "from-black via-gray-800 to-cyan-400",
112
-
twitter: "from-blue-400 to-blue-600",
113
-
instagram: "from-pink-500 via-purple-500 to-orange-500",
114
-
};
115
-
return colors[platform] || "from-gray-400 to-gray-600";
116
-
};
117
-
118
-
const tabs = [
119
-
{ id: "upload" as TabId, icon: Upload, label: "Upload" },
120
-
{ id: "history" as TabId, icon: History, label: "History" },
121
-
{ id: "settings" as TabId, icon: Settings, label: "Settings" },
122
-
{ id: "guides" as TabId, icon: BookOpen, label: "Guides" },
123
-
{ id: "apps" as TabId, icon: Grid3x3, label: "Apps" },
124
-
];
125
-
126
83
return (
127
-
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
84
+
<div className="min-h-screen">
128
85
<SetupWizard
129
86
isOpen={showWizard}
130
87
onClose={() => setShowWizard(false)}
···
144
101
onToggleTheme={onToggleTheme}
145
102
onToggleMotion={onToggleMotion}
146
103
/>
147
-
148
-
{/* Tab Navigation */}
149
-
<div className="max-w-6xl mx-auto">
150
-
<div className="overflow-x-auto scrollbar-hide px-4">
151
-
<div className="flex space-x-1 border-b border-gray-200 dark:border-gray-700 min-w-max">
152
-
{tabs.map((tab) => {
153
-
const Icon = tab.icon;
154
-
return (
155
-
<button
156
-
key={tab.id}
157
-
onClick={() => setActiveTab(tab.id)}
158
-
className={`flex items-center space-x-2 px-4 py-3 border-b-2 transition-all whitespace-nowrap ${
159
-
activeTab === tab.id
160
-
? "border-orange-500 dark:border-amber-500 text-orange-650 dark:text-amber-400"
161
-
: "border-transparent text-purple-750 dark:text-cyan-250 hover:text-purple-900 dark:hover:text-cyan-100"
162
-
}`}
163
-
>
164
-
<Icon className="w-4 h-4" />
165
-
<span className="font-medium">{tab.label}</span>
166
-
</button>
167
-
);
168
-
})}
169
-
</div>
170
-
</div>
171
-
</div>
172
104
</div>
173
105
174
-
{/* Tab Content */}
175
106
<div className="max-w-6xl mx-auto px-4 py-8">
176
-
{activeTab === "upload" && (
177
-
<div className="space-y-6">
178
-
{/* Setup Assistant Banner - Only show if wizard not completed */}
179
-
{!userSettings.wizardCompleted && (
180
-
<div className="bg-firefly-banner dark:bg-firefly-banner-dark rounded-2xl p-6 text-white">
181
-
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
182
-
<div className="flex-1">
183
-
<h2 className="text-2xl font-bold mb-2">
184
-
Need help getting started?
185
-
</h2>
186
-
<p className="text-white/90">
187
-
Run the setup assistant to configure your preferences in
188
-
minutes.
189
-
</p>
190
-
</div>
191
-
<button
192
-
onClick={() => setShowWizard(true)}
193
-
className="bg-white text-slate-900 px-6 py-3 rounded-xl font-semibold hover:bg-slate-100 transition-all flex items-center space-x-2 whitespace-nowrap shadow-lg"
194
-
>
195
-
<span>Start Setup</span>
196
-
<ChevronRight className="w-4 h-4" />
197
-
</button>
198
-
</div>
199
-
</div>
200
-
)}
107
+
<div className="max-w-6xl mx-auto bg-slate-100/50 dark:bg-slate-900/50 backdrop-blur-xl rounded-3xl p-3 border-2 border-cyan-500/30 dark:border-purple-500/30 mb-8">
108
+
{/* Tab Navigation */}
109
+
<TabNavigation activeTab={activeTab} onTabChange={setActiveTab} />
201
110
202
-
{/* Upload Section */}
203
-
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6 border-2 border-slate-200 dark:border-slate-700">
204
-
<div className="flex items-center justify-between mb-4">
205
-
<div className="flex items-center space-x-3">
206
-
<div className="w-12 h-12 bg-gradient-to-br from-firefly-amber via-firefly-orange to-firefly-pink rounded-xl flex items-center justify-center shadow-md">
207
-
<Upload className="w-6 h-6 text-white" />
208
-
</div>
209
-
<div>
210
-
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">
211
-
Upload Following Data
212
-
</h2>
213
-
<p className="text-sm text-gray-600 dark:text-gray-400">
214
-
Find your people on the ATmosphere
215
-
</p>
216
-
</div>
217
-
</div>
218
-
{userSettings.wizardCompleted && (
219
-
<button
220
-
onClick={() => setShowWizard(true)}
221
-
className="text-sm text-firefly-orange hover:text-firefly-pink font-medium transition-colors flex items-center space-x-1"
222
-
>
223
-
<Settings className="w-4 h-4" />
224
-
<span>Reconfigure</span>
225
-
</button>
226
-
)}
227
-
</div>
228
-
229
-
<PlatformSelector onPlatformSelect={handlePlatformSelect} />
230
-
231
-
<input
232
-
id="file-upload"
233
-
ref={fileInputRef}
234
-
type="file"
235
-
accept=".txt,.json,.html,.zip"
236
-
onChange={(e) => onFileUpload(e, selectedPlatform || "tiktok")}
237
-
className="sr-only"
238
-
aria-label="Upload following data file"
111
+
{/* Tab Content */}
112
+
<div>
113
+
{activeTab === "upload" && (
114
+
<UploadTab
115
+
wizardCompleted={userSettings.wizardCompleted}
116
+
onShowWizard={() => setShowWizard(true)}
117
+
onPlatformSelect={setSelectedPlatform}
118
+
onFileUpload={onFileUpload}
119
+
selectedPlatform={selectedPlatform}
239
120
/>
240
-
</div>
241
-
</div>
242
-
)}
243
-
244
-
{/* History Tab */}
245
-
{activeTab === "history" && (
246
-
<div className="bg-white/95 dark:bg-slate-800/95 backdrop-blur-xl rounded-2xl shadow-lg p-6 border-2 border-slate-200 dark:border-slate-700">
247
-
<div className="flex items-center space-x-3 mb-6">
248
-
<Sparkles className="w-6 h-6 text-firefly-amber" />
249
-
<h2 className="text-xl font-bold text-slate-900 dark:text-slate-100">
250
-
Your Light Trail
251
-
</h2>
252
-
</div>
121
+
)}
253
122
254
-
{isLoading ? (
255
-
<div className="space-y-3">
256
-
{[...Array(3)].map((_, i) => (
257
-
<div
258
-
key={i}
259
-
className="animate-pulse flex items-center space-x-4 p-4 bg-slate-50 dark:bg-slate-700 rounded-xl"
260
-
>
261
-
<div className="w-12 h-12 bg-slate-200 dark:bg-slate-600 rounded-xl" />
262
-
<div className="flex-1 space-y-2">
263
-
<div className="h-4 bg-slate-200 dark:bg-slate-600 rounded w-3/4" />
264
-
<div className="h-3 bg-slate-200 dark:bg-slate-600 rounded w-1/2" />
265
-
</div>
266
-
</div>
267
-
))}
268
-
</div>
269
-
) : uploads.length === 0 ? (
270
-
<div className="text-center py-12">
271
-
<Upload className="w-16 h-16 text-slate-300 dark:text-slate-600 mx-auto mb-4" />
272
-
<p className="text-slate-600 dark:text-slate-400 font-medium">
273
-
No previous uploads yet
274
-
</p>
275
-
<p className="text-sm text-slate-500 dark:text-slate-500 mt-2">
276
-
Upload your first file to get started
277
-
</p>
278
-
</div>
279
-
) : (
280
-
<div className="space-y-3">
281
-
{uploads.map((upload) => {
282
-
const destApp =
283
-
ATPROTO_APPS[
284
-
userSettings.platformDestinations[
285
-
upload.sourcePlatform as keyof typeof userSettings.platformDestinations
286
-
]
287
-
];
288
-
return (
289
-
<button
290
-
key={upload.uploadId}
291
-
onClick={() => onLoadUpload(upload.uploadId)}
292
-
className="w-full flex items-start space-x-4 p-4 bg-slate-50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-900/70 rounded-xl transition-all text-left border-2 border-slate-200 dark:border-slate-700 hover:border-firefly-orange dark:hover:border-firefly-orange shadow-md hover:shadow-lg"
293
-
>
294
-
<div
295
-
className={`w-12 h-12 bg-gradient-to-r ${getPlatformColor(upload.sourcePlatform)} rounded-xl flex items-center justify-center flex-shrink-0 shadow-md`}
296
-
>
297
-
<Sparkles className="w-6 h-6 text-white" />
298
-
</div>
299
-
<div className="flex-1 min-w-0">
300
-
<div className="flex flex-wrap items-start justify-between gap-x-4 gap-y-2 mb-1">
301
-
<div className="font-semibold text-slate-900 dark:text-slate-100 capitalize">
302
-
{upload.sourcePlatform}
303
-
</div>
304
-
<div className="flex items-center gap-2 flex-shrink-0">
305
-
<span className="text-xs px-2 py-0.5 bg-firefly-amber/20 dark:bg-firefly-amber/30 text-amber-900 dark:text-firefly-glow rounded-full font-medium border border-firefly-amber/20 dark:border-firefly-amber/50 whitespace-nowrap">
306
-
{upload.matchedUsers}{" "}
307
-
{upload.matchedUsers === 1
308
-
? "firefly"
309
-
: "fireflies"}
310
-
</span>
311
-
<div className="text-sm text-slate-600 dark:text-slate-400 font-medium whitespace-nowrap">
312
-
{Math.round(
313
-
(upload.matchedUsers / upload.totalUsers) * 100,
314
-
)}
315
-
%
316
-
</div>
317
-
</div>
318
-
</div>
319
-
<div className="text-sm text-slate-700 dark:text-slate-300">
320
-
{upload.totalUsers} users •{" "}
321
-
{formatDate(upload.createdAt)}
322
-
</div>
323
-
{destApp && (
324
-
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
325
-
Sent to {destApp.icon} {destApp.name}
326
-
</div>
327
-
)}
328
-
</div>
329
-
</button>
330
-
);
331
-
})}
332
-
</div>
123
+
{activeTab === "history" && (
124
+
<HistoryTab
125
+
uploads={uploads}
126
+
isLoading={isLoading}
127
+
userSettings={userSettings}
128
+
onLoadUpload={onLoadUpload}
129
+
/>
333
130
)}
334
-
</div>
335
-
)}
336
131
337
-
{/* Settings Tab */}
338
-
{activeTab === "settings" && (
339
-
<SettingsPage
340
-
userSettings={userSettings}
341
-
onSettingsUpdate={onSettingsUpdate}
342
-
onOpenWizard={() => setShowWizard(true)}
343
-
/>
344
-
)}
132
+
{activeTab === "settings" && (
133
+
<SettingsPage
134
+
userSettings={userSettings}
135
+
onSettingsUpdate={onSettingsUpdate}
136
+
onOpenWizard={() => setShowWizard(true)}
137
+
/>
138
+
)}
345
139
346
-
{/* Guides Tab - Placeholder */}
347
-
{activeTab === "guides" && (
348
-
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
349
-
<div className="flex items-center space-x-3 mb-6">
350
-
<BookOpen className="w-6 h-6 text-gray-600 dark:text-gray-400" />
351
-
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">
352
-
Platform Guides
353
-
</h2>
354
-
</div>
355
-
<p className="text-gray-600 dark:text-gray-400">
356
-
Export guides coming soon...
357
-
</p>
358
-
</div>
359
-
)}
140
+
{activeTab === "guides" && (
141
+
<PlaceholderTab
142
+
icon={BookOpen}
143
+
title="Platform Guides"
144
+
message="Export guides coming soon..."
145
+
/>
146
+
)}
360
147
361
-
{/* Apps Tab - Placeholder */}
362
-
{activeTab === "apps" && (
363
-
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
364
-
<div className="flex items-center space-x-3 mb-6">
365
-
<Grid3x3 className="w-6 h-6 text-gray-600 dark:text-gray-400" />
366
-
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">
367
-
ATmosphere Apps
368
-
</h2>
369
-
</div>
370
-
<p className="text-gray-600 dark:text-gray-400">
371
-
Apps directory coming soon...
372
-
</p>
148
+
{activeTab === "apps" && (
149
+
<PlaceholderTab
150
+
icon={Grid3x3}
151
+
title="ATmosphere Apps"
152
+
message="Apps directory coming soon..."
153
+
/>
154
+
)}
373
155
</div>
374
-
)}
156
+
</div>
375
157
</div>
376
158
</div>
377
159
);
+45
-64
src/pages/Login.tsx
+45
-64
src/pages/Login.tsx
···
1
1
import { useState } from "react";
2
-
import { Heart, Upload, Search, ArrowRight } from "lucide-react";
2
+
import { Heart, Upload, Search, ArrowRight, Sparkles } from "lucide-react";
3
3
import FireflyLogo from "../assets/at-firefly-logo.svg?react";
4
4
5
5
interface LoginPageProps {
···
39
39
</div>
40
40
</div>
41
41
42
-
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold text-slate-900 dark:text-white mb-3 md:mb-4">
42
+
<h1 className="text-5xl md:text-6xl font-bold bg-gradient-to-r from-purple-600 via-cyan-500 to-pink-500 dark:from-cyan-300 dark:via-purple-300 dark:to-pink-300 bg-clip-text text-transparent mb-3 md:mb-4">
43
43
ATlast
44
44
</h1>
45
-
<p className="text-lg md:text-xl lg:text-2xl text-slate-800 dark:text-slate-100 mb-2 font-medium">
45
+
<p className="text-xl md:text-2xl lg:text-2xl text-purple-900 dark:text-cyan-100 mb-2 font-medium">
46
46
Find Your Light in the ATmosphere
47
47
</p>
48
-
<p className="text-slate-700 dark:text-slate-300 mb-6">
48
+
<p className="text-purple-750 dark:text-cyan-250 mb-6">
49
49
Reconnect with your internet, one firefly at a time ✨
50
50
</p>
51
51
···
58
58
{[...Array(5)].map((_, i) => (
59
59
<div
60
60
key={i}
61
-
className="w-2 h-2 rounded-full bg-firefly-amber dark:bg-firefly-glow"
61
+
className="w-2 h-2 rounded-full bg-orange-500 dark:bg-amber-400"
62
62
style={{
63
63
opacity: 1 - i * 0.15,
64
64
animation: `float ${2 + i * 0.3}s ease-in-out infinite`,
···
68
68
))}
69
69
</div>
70
70
)}
71
-
72
-
{/* Privacy Notice - visible on mobile */}
73
-
<div className="md:hidden mt-6">
74
-
<p className="text-sm text-slate-600 dark:text-slate-400">
75
-
Your data is processed and stored by our servers. This helps you
76
-
find matches and reconnect with your community.
77
-
</p>
78
-
</div>
79
71
</div>
80
72
81
73
{/* Right: Login Card or Dashboard Button */}
82
74
<div className="w-full">
83
75
{session ? (
84
-
<div className="bg-white/95 dark:bg-slate-800/95 backdrop-blur-xl rounded-3xl shadow-2xl p-8 border-2 border-slate-200 dark:border-slate-700">
76
+
<div className="bg-white/50 dark:bg-slate-900/50 border-cyan-500/30 dark:border-purple-500/30 backdrop-blur-xl rounded-3xl p-8 border-2 shadow-2xl">
85
77
<div className="text-center mb-6">
86
-
<div className="w-16 h-16 bg-gradient-to-br from-firefly-amber via-firefly-orange to-firefly-pink rounded-full mx-auto mb-4 flex items-center justify-center shadow-md">
87
-
<Heart className="w-8 h-8 text-slate-900" />
88
-
</div>
89
-
<h2 className="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-2">
78
+
<h2 className="text-2xl font-bold text-purple-950 dark:text-cyan-50 mb-2">
90
79
You're logged in!
91
80
</h2>
92
-
<p className="text-slate-700 dark:text-slate-300">
81
+
<p className="text-purple-750 dark:text-cyan-250">
93
82
Welcome back, @{session.handle}
94
83
</p>
95
84
</div>
96
85
97
86
<button
98
87
onClick={() => onNavigate?.("home")}
99
-
className="w-full bg-gradient-to-r from-firefly-amber via-firefly-orange to-firefly-pink hover:from-amber-600 hover:via-orange-600 hover:to-pink-600 text-white py-4 rounded-xl font-bold text-lg transition-all shadow-lg hover:shadow-xl focus:ring-4 focus:ring-orange-300 dark:focus:ring-orange-800 focus:outline-none flex items-center justify-center space-x-2"
88
+
className="w-full bg-firefly-banner dark:bg-firefly-banner-dark text-white py-4 rounded-xl font-bold text-lg transition-all shadow-lg hover:shadow-xl focus:ring-4 focus:ring-orange-500 dark:focus:ring-amber-400 focus:outline-none flex items-center justify-center space-x-2"
100
89
>
101
90
<span>Go to Dashboard</span>
102
91
<ArrowRight className="w-5 h-5" />
103
92
</button>
104
93
</div>
105
94
) : (
106
-
<div className="bg-white/95 dark:bg-slate-800/95 backdrop-blur-xl rounded-3xl shadow-2xl p-6 md:p-8 border-2 border-slate-200 dark:border-slate-700">
107
-
<h2 className="text-xl md:text-2xl font-bold text-slate-900 dark:text-slate-100 mb-2 text-center">
95
+
<div className="bg-white/50 dark:bg-slate-900/50 border-cyan-500/30 dark:border-purple-500/30 backdrop-blur-xl rounded-3xl p-8 border-2 shadow-2xl">
96
+
<h2 className="text-2xl font-bold text-purple-950 dark:text-cyan-50 mb-2 text-center">
108
97
Light Up Your Network
109
98
</h2>
110
-
<p className="text-slate-700 dark:text-slate-300 text-center mb-6">
99
+
<p className="text-purple-750 dark:text-cyan-250 text-center mb-6">
111
100
Connect your ATmosphere account to begin
112
101
</p>
113
102
···
119
108
<div>
120
109
<label
121
110
htmlFor="atproto-handle"
122
-
className="block text-sm font-semibold text-slate-900 dark:text-slate-100 mb-2"
111
+
className="block text-sm font-semibold text-purple-900 dark:text-cyan-100 mb-2"
123
112
>
124
113
Your ATmosphere Handle
125
114
</label>
···
129
118
value={handle}
130
119
onChange={(e) => setHandle(e.target.value)}
131
120
placeholder="yourname.bsky.social"
132
-
className="w-full px-4 py-3 bg-slate-50 dark:bg-slate-900/50 border-2 border-slate-300 dark:border-slate-600 rounded-xl text-slate-900 dark:text-slate-100 placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-firefly-orange focus:border-transparent transition-all"
121
+
className="w-full px-4 py-3 bg-purple-50/50 dark:bg-slate-900/50 border-2 border-cyan-500/50 dark:border-purple-500/30 rounded-xl text-purple-900 dark:text-cyan-100 placeholder-purple-750/80 dark:placeholder-cyan-250/80 focus:outline-none focus:ring-2 focus:ring-orange-500 dark:focus:ring-amber-400 focus:border-transparent transition-all"
133
122
aria-required="true"
134
123
aria-describedby="handle-description"
135
124
/>
136
-
<p
125
+
{/*<p
137
126
id="handle-description"
138
127
className="text-xs text-slate-600 dark:text-slate-400 mt-2"
139
128
>
140
129
Enter your full ATmosphere handle (e.g.,
141
130
username.bsky.social or yourname.com)
142
-
</p>
131
+
</p>*/}
143
132
</div>
144
133
145
134
<button
146
135
type="submit"
147
-
className="w-full bg-gradient-to-r from-firefly-amber via-firefly-orange to-firefly-pink hover:from-amber-600 hover:via-orange-600 hover:to-pink-600 text-white py-4 rounded-xl font-bold text-lg transition-all shadow-lg hover:shadow-xl focus:ring-4 focus:ring-orange-300 dark:focus:ring-orange-800 focus:outline-none"
136
+
className="w-full bg-firefly-banner dark:bg-firefly-banner-dark text-white py-4 rounded-xl font-bold text-lg transition-all shadow-lg hover:shadow-xl focus:ring-4 focus:ring-orange-500 dark:focus:ring-amber-400 focus:outline-none"
148
137
aria-label="Connect to the ATmosphere"
149
138
>
150
-
Join the Swarm ✨
139
+
Join the Swarm
151
140
</button>
152
141
</form>
153
142
154
-
<div className="mt-6 pt-6 border-t-2 border-slate-200 dark:border-slate-700">
155
-
<div className="flex items-start space-x-2 text-sm text-slate-700 dark:text-slate-300">
143
+
<div className="mt-6 pt-6 border-t-2 border-cyan-500/30 dark:border-purple-500/30">
144
+
<div className="flex items-start space-x-2 text-sm text-purple-900 dark:text-cyan-100">
156
145
<svg
157
146
className="w-5 h-5 text-green-500 flex-shrink-0 mt-0.5"
158
147
fill="currentColor"
···
166
155
/>
167
156
</svg>
168
157
<div>
169
-
<p className="font-semibold text-slate-900 dark:text-slate-100">
158
+
<p className="font-semibold text-purple-950 dark:text-cyan-50">
170
159
Secure OAuth Connection
171
160
</p>
172
161
<p className="text-xs mt-1">
···
183
172
184
173
{/* Value Props */}
185
174
<div className="grid md:grid-cols-3 gap-4 md:gap-6 mb-12 md:mb-16 max-w-5xl mx-auto">
186
-
<div className="bg-white/95 dark:bg-slate-800/95 backdrop-blur-xl rounded-2xl p-6 shadow-lg border-2 border-slate-200 dark:border-slate-700 hover:border-firefly-orange dark:hover:border-firefly-orange transition-all">
187
-
<div className="w-12 h-12 bg-gradient-to-br from-firefly-amber to-firefly-orange rounded-xl flex items-center justify-center mb-4 shadow-md">
175
+
<div className="bg-white/50 border-cyan-500/30 hover:border-cyan-400 dark:bg-slate-900/50 dark:border-purple-500/30 dark:hover:border-purple-400 backdrop-blur-xl rounded-2xl p-6 border-2 transition-all hover:scale-105 shadow-lg">
176
+
<div className="w-12 h-12 bg-gradient-to-br from-amber-300 to-orange-600 rounded-xl flex items-center justify-center mb-4 shadow-md">
188
177
<Upload className="w-6 h-6 text-slate-900" />
189
178
</div>
190
-
<h3 className="text-lg font-bold text-slate-900 dark:text-slate-100 mb-2">
179
+
<h3 className="text-lg font-bold text-purple-950 dark:text-cyan-50 mb-2">
191
180
Share Your Light
192
181
</h3>
193
-
<p className="text-slate-700 dark:text-slate-300 text-sm leading-relaxed">
182
+
<p className="text-purple-750 dark:text-cyan-250 text-sm leading-relaxed">
194
183
Import your following lists. Your data stays private, your
195
184
connections shine bright.
196
185
</p>
197
186
</div>
198
187
199
-
<div className="bg-white/95 dark:bg-slate-800/95 backdrop-blur-xl rounded-2xl p-6 shadow-lg border-2 border-slate-200 dark:border-slate-700 hover:border-firefly-orange dark:hover:border-firefly-orange transition-all">
200
-
<div className="w-12 h-12 bg-gradient-to-br from-firefly-cyan to-blue-500 rounded-xl flex items-center justify-center mb-4 shadow-md">
188
+
<div className="bg-white/50 border-cyan-500/30 hover:border-cyan-400 dark:bg-slate-900/50 dark:border-purple-500/30 dark:hover:border-purple-400 backdrop-blur-xl rounded-2xl p-6 border-2 transition-all hover:scale-105 shadow-lg">
189
+
<div className="w-12 h-12 bg-gradient-to-br from-amber-300 to-orange-600 rounded-xl flex items-center justify-center mb-4 shadow-md">
201
190
<Search className="w-6 h-6 text-slate-900" />
202
191
</div>
203
-
<h3 className="text-lg font-bold text-slate-900 dark:text-slate-100 mb-2">
192
+
<h3 className="text-lg font-bold text-purple-950 dark:text-cyan-50 mb-2">
204
193
Find Your Swarm
205
194
</h3>
206
-
<p className="text-slate-700 dark:text-slate-300 text-sm leading-relaxed">
195
+
<p className="text-purple-750 dark:text-cyan-250 text-sm leading-relaxed">
207
196
Watch as fireflies light up - discover which friends have already
208
197
migrated to the ATmosphere.
209
198
</p>
210
199
</div>
211
200
212
-
<div className="bg-white/95 dark:bg-slate-800/95 backdrop-blur-xl rounded-2xl p-6 shadow-lg border-2 border-slate-200 dark:border-slate-700 hover:border-firefly-orange dark:hover:border-firefly-orange transition-all">
213
-
<div className="w-12 h-12 bg-gradient-to-br from-firefly-pink to-purple-500 rounded-xl flex items-center justify-center mb-4 shadow-md">
201
+
<div className="bg-white/50 border-cyan-500/30 hover:border-cyan-400 dark:bg-slate-900/50 dark:border-purple-500/30 dark:hover:border-purple-400 backdrop-blur-xl rounded-2xl p-6 border-2 transition-all hover:scale-105 shadow-lg">
202
+
<div className="w-12 h-12 bg-gradient-to-br from-amber-300 to-orange-600 rounded-xl flex items-center justify-center mb-4 shadow-md">
214
203
<Heart className="w-6 h-6 text-slate-900" />
215
204
</div>
216
-
<h3 className="text-lg font-bold text-slate-900 dark:text-slate-100 mb-2">
205
+
<h3 className="text-lg font-bold text-purple-950 dark:text-cyan-50 mb-2">
217
206
Sync Your Glow
218
207
</h3>
219
-
<p className="text-slate-700 dark:text-slate-300 text-sm leading-relaxed">
208
+
<p className="text-purple-750 dark:text-cyan-250 text-sm leading-relaxed">
220
209
Reconnect instantly. Follow everyone at once or pick and choose -
221
210
light up together.
222
211
</p>
223
212
</div>
224
213
</div>
225
214
226
-
{/* Privacy Notice - desktop only */}
227
-
<div className="hidden md:block text-center mb-8">
228
-
<p className="text-sm text-slate-600 dark:text-slate-400 max-w-2xl mx-auto">
229
-
Your data is processed and stored by our servers. This helps you
230
-
find matches and reconnect with your community.
231
-
</p>
232
-
</div>
233
-
234
215
{/* How It Works */}
235
216
<div className="max-w-4xl mx-auto">
236
-
<h2 className="text-2xl font-bold text-center text-slate-900 dark:text-slate-100 mb-8">
217
+
<h2 className="text-2xl font-bold text-center text-purple-950 dark:text-cyan-50 mb-8">
237
218
How It Works
238
219
</h2>
239
220
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
240
221
<div className="text-center">
241
222
<div
242
-
className="w-12 h-12 bg-firefly-orange text-white rounded-full flex items-center justify-center mx-auto mb-3 font-bold text-lg shadow-md"
223
+
className="w-12 h-12 bg-orange-500 text-white rounded-full flex items-center justify-center mx-auto mb-3 font-bold text-lg shadow-md"
243
224
aria-hidden="true"
244
225
>
245
226
1
246
227
</div>
247
-
<h3 className="font-semibold text-slate-900 dark:text-slate-100 mb-1">
228
+
<h3 className="font-semibold text-purple-950 dark:text-cyan-50 mb-1">
248
229
Connect
249
230
</h3>
250
-
<p className="text-sm text-slate-700 dark:text-slate-300">
231
+
<p className="text-sm text-purple-900 dark:text-cyan-100">
251
232
Sign in with your ATmosphere account
252
233
</p>
253
234
</div>
254
235
<div className="text-center">
255
236
<div
256
-
className="w-12 h-12 bg-firefly-pink text-white rounded-full flex items-center justify-center mx-auto mb-3 font-bold text-lg shadow-md"
237
+
className="w-12 h-12 bg-cyan-500 text-white rounded-full flex items-center justify-center mx-auto mb-3 font-bold text-lg shadow-md"
257
238
aria-hidden="true"
258
239
>
259
240
2
260
241
</div>
261
-
<h3 className="font-semibold text-slate-900 dark:text-slate-100 mb-1">
242
+
<h3 className="font-semibold text-purple-950 dark:text-cyan-50 mb-1">
262
243
Upload
263
244
</h3>
264
-
<p className="text-sm text-slate-700 dark:text-slate-300">
245
+
<p className="text-sm text-purple-900 dark:text-cyan-100">
265
246
Import your following data from other platforms
266
247
</p>
267
248
</div>
268
249
<div className="text-center">
269
250
<div
270
-
className="w-12 h-12 bg-firefly-cyan text-white rounded-full flex items-center justify-center mx-auto mb-3 font-bold text-lg shadow-md"
251
+
className="w-12 h-12 bg-pink-500 text-white rounded-full flex items-center justify-center mx-auto mb-3 font-bold text-lg shadow-md"
271
252
aria-hidden="true"
272
253
>
273
254
3
···
275
256
<h3 className="font-semibold text-slate-900 dark:text-slate-100 mb-1">
276
257
Match
277
258
</h3>
278
-
<p className="text-sm text-slate-700 dark:text-slate-300">
259
+
<p className="text-sm text-purple-900 dark:text-cyan-100">
279
260
We find your fireflies in the ATmosphere
280
261
</p>
281
262
</div>
282
263
<div className="text-center">
283
264
<div
284
-
className="w-12 h-12 bg-firefly-amber text-slate-900 rounded-full flex items-center justify-center mx-auto mb-3 font-bold text-lg shadow-md"
265
+
className="w-12 h-12 bg-amber-500 text-white rounded-full flex items-center justify-center mx-auto mb-3 font-bold text-lg shadow-md"
285
266
aria-hidden="true"
286
267
>
287
268
4
288
269
</div>
289
-
<h3 className="font-semibold text-slate-900 dark:text-slate-100 mb-1">
270
+
<h3 className="font-semibold text-purple-950 dark:text-cyan-50 mb-1">
290
271
Follow
291
272
</h3>
292
-
<p className="text-sm text-slate-700 dark:text-slate-300">
273
+
<p className="text-sm text-purple-900 dark:text-cyan-100">
293
274
Reconnect with your community
294
275
</p>
295
276
</div>
+58
-46
src/pages/Results.tsx
+58
-46
src/pages/Results.tsx
···
28
28
interface ResultsPageProps {
29
29
session: atprotoSession | null;
30
30
onLogout: () => void;
31
-
onNavigate: (step: 'home' | 'login') => void;
31
+
onNavigate: (step: "home" | "login") => void;
32
32
searchResults: SearchResult[];
33
33
expandedResults: Set<number>;
34
34
onToggleExpand: (index: number) => void;
···
66
66
reducedMotion = false,
67
67
isDark = false,
68
68
onToggleTheme,
69
-
onToggleMotion
69
+
onToggleMotion,
70
70
}: ResultsPageProps) {
71
71
const platform = PLATFORMS[sourcePlatform] || PLATFORMS.tiktok;
72
72
const PlatformIcon = platform.icon;
73
73
74
74
return (
75
75
<div className="min-h-screen pb-24">
76
-
<AppHeader
77
-
session={session}
78
-
onLogout={onLogout}
79
-
onNavigate={onNavigate}
76
+
<AppHeader
77
+
session={session}
78
+
onLogout={onLogout}
79
+
onNavigate={onNavigate}
80
80
currentStep={currentStep}
81
81
isDark={isDark}
82
82
reducedMotion={reducedMotion}
83
83
onToggleTheme={onToggleTheme}
84
84
onToggleMotion={onToggleMotion}
85
85
/>
86
-
86
+
87
87
{/* Platform Info Banner */}
88
88
<div className="bg-firefly-banner dark:bg-firefly-banner-dark text-white relative overflow-hidden">
89
89
{!reducedMotion && (
···
96
96
left: `${Math.random() * 100}%`,
97
97
top: `${Math.random() * 100}%`,
98
98
animation: `float ${2 + Math.random()}s ease-in-out infinite`,
99
-
animationDelay: `${Math.random()}s`
99
+
animationDelay: `${Math.random()}s`,
100
100
}}
101
101
/>
102
102
))}
···
109
109
<Sparkles className="w-6 h-6 text-white" />
110
110
</div>
111
111
<div>
112
-
<h2 className="text-xl font-bold">{totalFound} Connections Found!</h2>
112
+
<h2 className="text-xl font-bold">
113
+
{totalFound} Connections Found!
114
+
</h2>
113
115
<p className="text-white/95 text-sm">
114
116
From {searchResults.length} {platform.name} follows
115
117
</p>
···
126
128
</div>
127
129
128
130
{/* Action Buttons */}
129
-
<div className="bg-white/95 dark:bg-slate-800/95 border-b-2 border-slate-200 dark:border-slate-700 sticky top-0 z-10 backdrop-blur-sm">
131
+
<div className="bg-white/95 dark:bg-slate-900 border-b-2 border-slate-200 dark:border-purple-500/30 sticky top-0 z-10 backdrop-blur-sm">
130
132
<div className="max-w-3xl mx-auto px-4 py-3 flex space-x-2">
131
133
<button
132
134
onClick={onSelectAll}
133
-
className="flex-1 bg-orange-600 hover:bg-orange-700 text-white py-3 rounded-xl text-sm font-semibold transition-all shadow-md hover:shadow-lg focus:outline-none focus:ring-4 focus:ring-orange-300 dark:focus:ring-orange-800"
135
+
className="flex-1 bg-orange-600 hover:bg-orange-500 text-white py-3 rounded-xl text-sm font-semibold transition-all shadow-md hover:shadow-lg"
134
136
type="button"
135
137
>
136
138
Select All
···
147
149
148
150
{/* Feed Results */}
149
151
<div className="max-w-3xl mx-auto px-4 py-4 space-y-4">
150
-
{[...searchResults].sort((a, b) => {
151
-
// Sort logic here, match sortSearchResults function
152
-
const aHasMatches = a.atprotoMatches.length > 0 ? 0 : 1;
153
-
const bHasMatches = b.atprotoMatches.length > 0 ? 0 : 1;
154
-
if (aHasMatches !== bHasMatches) return aHasMatches - bHasMatches;
155
-
156
-
if (a.atprotoMatches.length > 0 && b.atprotoMatches.length > 0) {
157
-
const aTopPosts = a.atprotoMatches[0]?.postCount || 0;
158
-
const bTopPosts = b.atprotoMatches[0]?.postCount || 0;
159
-
if (aTopPosts !== bTopPosts) return bTopPosts - aTopPosts;
160
-
161
-
const aTopFollowers = a.atprotoMatches[0]?.followerCount || 0;
162
-
const bTopFollowers = b.atprotoMatches[0]?.followerCount || 0;
163
-
if (aTopFollowers !== bTopFollowers) return bTopFollowers - aTopFollowers;
164
-
}
165
-
166
-
return a.sourceUser.username.localeCompare(b.sourceUser.username);
167
-
}).map((result, idx) => {
168
-
// Find the original index in unsorted array
169
-
const originalIndex = searchResults.findIndex(r => r.sourceUser.username === result.sourceUser.username);
170
-
return (
171
-
<SearchResultCard
172
-
key={originalIndex}
173
-
result={result}
174
-
resultIndex={originalIndex} // Use original index for state updates
175
-
isExpanded={expandedResults.has(originalIndex)}
176
-
onToggleExpand={() => onToggleExpand(originalIndex)}
177
-
onToggleMatchSelection={(did) => onToggleMatchSelection(originalIndex, did)}
178
-
sourcePlatform={sourcePlatform}
179
-
/>
180
-
);
181
-
})}
152
+
{[...searchResults]
153
+
.sort((a, b) => {
154
+
// Sort logic here, match sortSearchResults function
155
+
const aHasMatches = a.atprotoMatches.length > 0 ? 0 : 1;
156
+
const bHasMatches = b.atprotoMatches.length > 0 ? 0 : 1;
157
+
if (aHasMatches !== bHasMatches) return aHasMatches - bHasMatches;
158
+
159
+
if (a.atprotoMatches.length > 0 && b.atprotoMatches.length > 0) {
160
+
const aTopPosts = a.atprotoMatches[0]?.postCount || 0;
161
+
const bTopPosts = b.atprotoMatches[0]?.postCount || 0;
162
+
if (aTopPosts !== bTopPosts) return bTopPosts - aTopPosts;
163
+
164
+
const aTopFollowers = a.atprotoMatches[0]?.followerCount || 0;
165
+
const bTopFollowers = b.atprotoMatches[0]?.followerCount || 0;
166
+
if (aTopFollowers !== bTopFollowers)
167
+
return bTopFollowers - aTopFollowers;
168
+
}
169
+
170
+
return a.sourceUser.username.localeCompare(b.sourceUser.username);
171
+
})
172
+
.map((result, idx) => {
173
+
// Find the original index in unsorted array
174
+
const originalIndex = searchResults.findIndex(
175
+
(r) => r.sourceUser.username === result.sourceUser.username,
176
+
);
177
+
return (
178
+
<SearchResultCard
179
+
key={originalIndex}
180
+
result={result}
181
+
resultIndex={originalIndex} // Use original index for state updates
182
+
isExpanded={expandedResults.has(originalIndex)}
183
+
onToggleExpand={() => onToggleExpand(originalIndex)}
184
+
onToggleMatchSelection={(did) =>
185
+
onToggleMatchSelection(originalIndex, did)
186
+
}
187
+
sourcePlatform={sourcePlatform}
188
+
/>
189
+
);
190
+
})}
182
191
</div>
183
192
184
193
{/* Fixed Bottom Action Bar */}
···
188
197
<button
189
198
onClick={onFollowSelected}
190
199
disabled={isFollowing}
191
-
className="w-full bg-firefly-banner dark:bg-firefly-banner-dark text-white hover:from-amber-600 hover:via-orange-600 hover:to-pink-600 text-white py-5 rounded-2xl font-bold text-lg transition-all shadow-2xl hover:shadow-3xl flex items-center justify-center space-x-3 hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 focus:outline-none focus:ring-4 focus:ring-orange-300 dark:focus:ring-orange-800"
200
+
className="w-full bg-firefly-banner dark:bg-firefly-banner-dark text-white hover:from-amber-600 hover:via-orange-600 hover:to-pink-600 py-5 rounded-2xl font-bold text-lg transition-all shadow-2xl hover:shadow-3xl flex items-center justify-center space-x-3 hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
192
201
>
193
202
<Sparkles className="w-6 h-6" />
194
-
<span>Light Up {totalSelected} Connection{totalSelected === 1 ? '' : 's'} ✨</span>
203
+
<span>
204
+
Light Up {totalSelected} Connection
205
+
{totalSelected === 1 ? "" : "s"} ✨
206
+
</span>
195
207
</button>
196
208
</div>
197
209
</div>
198
210
)}
199
211
</div>
200
212
);
201
-
}
213
+
}
+197
-181
src/pages/Settings.tsx
+197
-181
src/pages/Settings.tsx
···
1
-
import { Settings as SettingsIcon, Sparkles, Shield, Bell, Trash2, Download, ChevronRight } from "lucide-react";
1
+
import {
2
+
Settings as SettingsIcon,
3
+
Sparkles,
4
+
Shield,
5
+
Trash2,
6
+
Download,
7
+
ChevronRight,
8
+
} from "lucide-react";
2
9
import { PLATFORMS } from "../constants/platforms";
3
10
import { ATPROTO_APPS } from "../constants/atprotoApps";
4
11
import type { UserSettings, PlatformDestinations } from "../types/settings";
···
9
16
onOpenWizard: () => void;
10
17
}
11
18
12
-
export default function SettingsPage({ userSettings, onSettingsUpdate, onOpenWizard }: SettingsPageProps) {
19
+
export default function SettingsPage({
20
+
userSettings,
21
+
onSettingsUpdate,
22
+
onOpenWizard,
23
+
}: SettingsPageProps) {
13
24
const handleDestinationChange = (platform: string, destination: string) => {
14
25
onSettingsUpdate({
15
26
platformDestinations: {
···
21
32
22
33
const handleExportSettings = () => {
23
34
const dataStr = JSON.stringify(userSettings, null, 2);
24
-
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
25
-
const exportFileDefaultName = 'atlast-settings.json';
26
-
27
-
const linkElement = document.createElement('a');
28
-
linkElement.setAttribute('href', dataUri);
29
-
linkElement.setAttribute('download', exportFileDefaultName);
35
+
const dataUri =
36
+
"data:application/json;charset=utf-8," + encodeURIComponent(dataStr);
37
+
const exportFileDefaultName = "atlast-settings.json";
38
+
39
+
const linkElement = document.createElement("a");
40
+
linkElement.setAttribute("href", dataUri);
41
+
linkElement.setAttribute("download", exportFileDefaultName);
30
42
linkElement.click();
31
43
};
32
44
33
45
const handleResetSettings = () => {
34
-
if (confirm('Are you sure you want to reset all settings to defaults? This cannot be undone.')) {
35
-
// Import DEFAULT_SETTINGS
36
-
const { DEFAULT_SETTINGS } = require('../types/settings');
46
+
if (
47
+
confirm(
48
+
"Are you sure you want to reset all settings to defaults? This cannot be undone.",
49
+
)
50
+
) {
51
+
const { DEFAULT_SETTINGS } = require("../types/settings");
37
52
onSettingsUpdate({
38
53
...DEFAULT_SETTINGS,
39
-
wizardCompleted: true, // Keep wizard completed
54
+
wizardCompleted: true,
40
55
});
41
56
}
42
57
};
43
58
44
59
return (
45
-
<div className="space-y-6">
46
-
{/* Setup Wizard Card */}
47
-
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6 border-2 border-slate-200 dark:border-slate-700">
60
+
<div className="space-y-0">
61
+
{/* Setup Assistant Section */}
62
+
<div className="p-6 border-b-2 border-cyan-500/30 dark:border-purple-500/30">
48
63
<div className="flex items-center space-x-3 mb-4">
49
-
<div className="w-12 h-12 bg-gradient-to-br from-firefly-amber via-firefly-orange to-firefly-pink rounded-xl flex items-center justify-center shadow-md">
50
-
<Sparkles className="w-6 h-6 text-white" />
51
-
</div>
52
64
<div>
53
-
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">Setup Assistant</h2>
54
-
<p className="text-sm text-gray-600 dark:text-gray-400">Quick configuration wizard</p>
65
+
<h2 className="text-xl font-bold text-purple-950 dark:text-cyan-50">
66
+
Setup Assistant
67
+
</h2>
68
+
<p className="text-sm text-purple-750 dark:text-cyan-250">
69
+
Quick configuration wizard
70
+
</p>
55
71
</div>
56
72
</div>
57
-
73
+
58
74
<button
59
75
onClick={onOpenWizard}
60
-
className="w-full p-4 bg-gradient-to-r from-firefly-amber/10 via-firefly-orange/10 to-firefly-pink/10 border-2 border-firefly-orange/30 rounded-xl hover:border-firefly-orange hover:shadow-md transition-all text-left"
76
+
className="w-full flex items-start space-x-4 p-4 bg-purple-100/20 dark:bg-slate-900/50 hover:bg-purple-100/40 dark:hover:bg-slate-900/70 rounded-xl transition-all text-left border-2 border-orange-650/50 dark:border-amber-400/50 hover:border-orange-500 dark:hover:border-amber-400 shadow-md hover:shadow-lg"
61
77
>
62
-
<div className="flex items-center justify-between">
78
+
<div className="w-12 h-12 bg-gradient-to-r from-firefly-amber via-firefly-orange to-firefly-pink rounded-xl flex items-center justify-center flex-shrink-0 shadow-md">
79
+
<SettingsIcon className="w-6 h-6 text-white" />
80
+
</div>
81
+
<div className="flex-1 min-w-0">
82
+
<div className="flex flex-wrap items-start justify-between gap-x-4 gap-y-2 mb-1">
83
+
<div className="font-semibold text-purple-950 dark:text-cyan-50 leading-tight">
84
+
Run Setup Wizard
85
+
</div>
86
+
</div>
87
+
<p className="text-sm text-purple-750 dark:text-cyan-250 leading-tight">
88
+
Configure platform destinations, privacy, and automation settings
89
+
</p>
90
+
</div>
91
+
<ChevronRight className="w-5 h-5 text-purple-500 dark:text-cyan-400 flex-shrink-0 self-center" />
92
+
</button>
93
+
94
+
{/* Current Configuration */}
95
+
<div className="mt-2 py-2 px-3">
96
+
<h3 className="font-semibold text-purple-950 dark:text-cyan-50 mb-3">
97
+
Current Configuration
98
+
</h3>
99
+
<div className="gap-8 flex flex-wrap text-sm">
100
+
<div>
101
+
<div className="text-purple-750 dark:text-cyan-250 mb-1">
102
+
Data Storage
103
+
</div>
104
+
<div className="font-medium text-purple-950 dark:text-cyan-50">
105
+
{userSettings.saveData ? "✅ Enabled" : "❌ Disabled"}
106
+
</div>
107
+
</div>
108
+
<div>
109
+
<div className="text-purple-750 dark:text-cyan-250 mb-1">
110
+
Automation
111
+
</div>
112
+
<div className="font-medium text-purple-950 dark:text-cyan-50">
113
+
{userSettings.enableAutomation
114
+
? `✅ ${userSettings.automationFrequency}`
115
+
: "❌ Disabled"}
116
+
</div>
117
+
</div>
63
118
<div>
64
-
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1">Run Setup Wizard</h3>
65
-
<p className="text-sm text-gray-600 dark:text-gray-400">
66
-
Configure platform destinations, privacy, and automation settings
67
-
</p>
119
+
<div className="text-purple-750 dark:text-cyan-250 mb-1">
120
+
Wizard
121
+
</div>
122
+
<div className="font-medium text-purple-950 dark:text-cyan-50">
123
+
{userSettings.wizardCompleted ? "✅ Completed" : "⏳ Pending"}
124
+
</div>
68
125
</div>
69
-
<ChevronRight className="w-5 h-5 text-firefly-orange flex-shrink-0" />
70
126
</div>
71
-
</button>
127
+
</div>
72
128
</div>
73
129
74
-
{/* Platform Destinations */}
75
-
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6 border-2 border-slate-200 dark:border-slate-700">
130
+
{/* Match Destinations Section */}
131
+
<div className="p-6 border-b-2 border-cyan-500/30 dark:border-purple-500/30">
76
132
<div className="flex items-center space-x-3 mb-4">
77
-
<div className="w-12 h-12 bg-gradient-to-br from-firefly-cyan to-firefly-pink rounded-xl flex items-center justify-center shadow-md">
78
-
<SettingsIcon className="w-6 h-6 text-white" />
79
-
</div>
80
133
<div>
81
-
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">Match Destinations</h2>
82
-
<p className="text-sm text-gray-600 dark:text-gray-400">Where matches should go for each platform</p>
134
+
<h2 className="text-xl font-bold text-purple-950 dark:text-cyan-50">
135
+
Match Destinations
136
+
</h2>
137
+
<p className="text-sm text-purple-750 dark:text-cyan-250">
138
+
Where matches should go for each platform
139
+
</p>
83
140
</div>
84
141
</div>
85
-
86
-
<div className="space-y-3">
142
+
143
+
<div className="mt-3 px-3 py-2 rounded-lg border border-orange-650/50 dark:border-amber-400/50">
144
+
<p className="text-sm text-purple-900 dark:text-cyan-100">
145
+
💡 <strong>Tip:</strong> Choose different apps for different
146
+
platforms based on content type. For example, send TikTok matches to
147
+
Spark for video content.
148
+
</p>
149
+
</div>
150
+
151
+
<div className="py-2 space-y-0">
87
152
{Object.entries(PLATFORMS).map(([key, p]) => {
88
153
const Icon = p.icon;
89
-
const currentDestination = userSettings.platformDestinations[key as keyof PlatformDestinations];
90
-
const destinationApp = ATPROTO_APPS[currentDestination];
91
-
154
+
const currentDestination =
155
+
userSettings.platformDestinations[
156
+
key as keyof PlatformDestinations
157
+
];
158
+
92
159
return (
93
-
<div key={key} className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-900 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
160
+
<div
161
+
key={key}
162
+
className="flex items-center justify-between px-3 py-2 rounded-xl transition-colors"
163
+
>
94
164
<div className="flex items-center space-x-3 flex-1">
95
-
<Icon className="w-6 h-6 text-gray-700 dark:text-gray-300 flex-shrink-0" />
165
+
<Icon className="w-6 h-6 text-purple-950 dark:text-cyan-50 flex-shrink-0" />
96
166
<div className="flex-1 min-w-0">
97
-
<div className="font-medium text-gray-900 dark:text-gray-100">{p.name}</div>
98
-
<div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
99
-
Currently: {destinationApp?.icon} {destinationApp?.name}
167
+
<div className="font-medium text-purple-950 dark:text-cyan-50">
168
+
{p.name}
100
169
</div>
101
170
</div>
102
171
</div>
103
172
<select
104
173
value={currentDestination}
105
174
onChange={(e) => handleDestinationChange(key, e.target.value)}
106
-
className="px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-sm text-gray-900 dark:text-gray-100 hover:border-firefly-orange focus:outline-none focus:ring-2 focus:ring-firefly-orange transition-colors"
175
+
className="px-3 py-2 bg-white dark:bg-slate-800 border border-cyan-500/30 dark:border-purple-500/30 rounded-lg text-sm text-purple-950 dark:text-cyan-50 hover:border-cyan-400 dark:hover:border-purple-400 focus:outline-none focus:ring-2 focus:ring-orange-500 dark:focus:ring-amber-400 transition-colors"
107
176
>
108
177
{Object.values(ATPROTO_APPS).map((app) => (
109
178
<option key={app.id} value={app.id}>
···
115
184
);
116
185
})}
117
186
</div>
118
-
119
-
<div className="mt-4 p-3 bg-firefly-amber/10 dark:bg-firefly-amber/20 rounded-lg border border-firefly-amber/30">
120
-
<p className="text-sm text-gray-700 dark:text-gray-300">
121
-
💡 <strong>Tip:</strong> Choose different apps for different platforms based on content type.
122
-
For example, send TikTok matches to Spark for video content.
123
-
</p>
124
-
</div>
125
187
</div>
126
188
127
-
{/* Privacy & Data */}
128
-
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6 border-2 border-slate-200 dark:border-slate-700">
189
+
{/* Privacy & Data Section */}
190
+
<div className="p-6">
129
191
<div className="flex items-center space-x-3 mb-4">
130
-
<div className="w-12 h-12 bg-gradient-to-br from-firefly-cyan to-firefly-orange rounded-xl flex items-center justify-center shadow-md">
131
-
<Shield className="w-6 h-6 text-white" />
132
-
</div>
133
192
<div>
134
-
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">Privacy & Data</h2>
135
-
<p className="text-sm text-gray-600 dark:text-gray-400">Control how your data is stored</p>
193
+
<h2 className="text-xl font-bold text-purple-950 dark:text-cyan-50">
194
+
Privacy & Data
195
+
</h2>
196
+
<p className="text-sm text-purple-750 dark:text-cyan-250">
197
+
Control how your data is stored
198
+
</p>
136
199
</div>
137
200
</div>
138
-
139
-
<div className="space-y-3">
140
-
<div className="p-4 bg-firefly-cyan/10 dark:bg-firefly-cyan/20 rounded-xl border border-firefly-cyan/30">
201
+
202
+
<div className="px-3 space-y-4">
203
+
{/* Save Data Toggle */}
204
+
<div className="">
141
205
<div className="flex items-start justify-between">
142
206
<div className="flex-1">
143
-
<div className="font-medium text-gray-900 dark:text-gray-100 mb-1">Save my data</div>
144
-
<p className="text-sm text-gray-600 dark:text-gray-400">
145
-
Store your following lists for periodic re-checking and new match notifications
207
+
<div className="font-medium text-purple-950 dark:text-cyan-50 mb-1">
208
+
Save my data
209
+
</div>
210
+
<p className="text-sm text-purple-900 dark:text-cyan-100">
211
+
Store your following lists for periodic re-checking and new
212
+
match notifications
146
213
</p>
147
214
</div>
148
215
<label className="relative inline-flex items-center cursor-pointer ml-4">
149
-
<input
150
-
type="checkbox"
216
+
<input
217
+
type="checkbox"
151
218
checked={userSettings.saveData}
152
-
onChange={(e) => onSettingsUpdate({ saveData: e.target.checked })}
153
-
className="sr-only peer"
219
+
onChange={(e) =>
220
+
onSettingsUpdate({ saveData: e.target.checked })
221
+
}
222
+
className="sr-only peer"
154
223
/>
155
-
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-firefly-orange/30 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-firefly-orange"></div>
224
+
<div className="w-11 h-6 bg-gray-400 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-orange-650/50 dark:peer-focus:ring-amber-400/50 rounded-full peer dark:bg-gray-600 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-700 peer-checked:bg-orange-500 dark:peer-checked:bg-orange-400"></div>
156
225
</label>
157
226
</div>
158
227
</div>
159
228
160
-
{!userSettings.saveData && (
161
-
<div className="p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
162
-
<p className="text-sm text-yellow-800 dark:text-yellow-200">
163
-
⚠️ <strong>Note:</strong> Disabling data storage will prevent periodic checks and automation features.
164
-
</p>
165
-
</div>
166
-
)}
167
-
</div>
168
-
</div>
169
-
170
-
{/* Automation */}
171
-
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6 border-2 border-slate-200 dark:border-slate-700">
172
-
<div className="flex items-center space-x-3 mb-4">
173
-
<div className="w-12 h-12 bg-gradient-to-br from-firefly-pink to-firefly-orange rounded-xl flex items-center justify-center shadow-md">
174
-
<Bell className="w-6 h-6 text-white" />
175
-
</div>
176
-
<div>
177
-
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">Automation</h2>
178
-
<p className="text-sm text-gray-600 dark:text-gray-400">Automated checks and notifications</p>
179
-
</div>
180
-
</div>
181
-
182
-
<div className="space-y-3">
183
-
<div className="p-4 bg-firefly-pink/10 dark:bg-firefly-pink/20 rounded-xl border border-firefly-pink/30">
184
-
<div className="flex items-start justify-between">
229
+
{/* Automation Toggle */}
230
+
<div className="">
231
+
<div className="flex items-start justify-between mb-4">
185
232
<div className="flex-1">
186
-
<div className="font-medium text-gray-900 dark:text-gray-100 mb-1">Notify about new matches</div>
187
-
<p className="text-sm text-gray-600 dark:text-gray-400">
233
+
<div className="font-medium text-purple-950 dark:text-cyan-50 mb-1">
234
+
Notify about new matches
235
+
</div>
236
+
<p className="text-sm text-purple-900 dark:text-cyan-100">
188
237
Get DMs when people you follow join the ATmosphere
189
238
</p>
190
239
</div>
191
240
<label className="relative inline-flex items-center cursor-pointer ml-4">
192
-
<input
193
-
type="checkbox"
241
+
<input
242
+
type="checkbox"
194
243
checked={userSettings.enableAutomation}
195
-
onChange={(e) => onSettingsUpdate({ enableAutomation: e.target.checked })}
244
+
onChange={(e) =>
245
+
onSettingsUpdate({ enableAutomation: e.target.checked })
246
+
}
196
247
className="sr-only peer"
197
248
disabled={!userSettings.saveData}
198
249
/>
199
-
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-firefly-pink/30 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-firefly-pink peer-disabled:opacity-50 peer-disabled:cursor-not-allowed"></div>
250
+
<div className="w-11 h-6 bg-gray-400 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-orange-650/50 dark:peer-focus:ring-amber-400/50 rounded-full peer dark:bg-gray-600 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-700 peer-checked:bg-orange-500 dark:peer-checked:bg-orange-400"></div>
200
251
</label>
201
252
</div>
202
-
253
+
203
254
{userSettings.enableAutomation && (
204
-
<div className="mt-4 pt-4 border-t border-firefly-pink/20">
205
-
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
206
-
Check frequency
255
+
<div className="flex items-center gap-3 px-6">
256
+
<label className="text-sm font-medium text-purple-950 dark:text-cyan-50 whitespace-nowrap">
257
+
Frequency
207
258
</label>
208
259
<select
209
260
value={userSettings.automationFrequency}
210
-
onChange={(e) => onSettingsUpdate({ automationFrequency: e.target.value as 'weekly' | 'monthly' | 'quarterly' })}
211
-
className="w-full px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-sm text-gray-900 dark:text-gray-100 hover:border-firefly-pink focus:outline-none focus:ring-2 focus:ring-firefly-pink"
261
+
onChange={(e) =>
262
+
onSettingsUpdate({
263
+
automationFrequency: e.target.value as
264
+
| "Weekly"
265
+
| "Monthly"
266
+
| "Quarterly",
267
+
})
268
+
}
269
+
className="flex-1 px-3 py-2 bg-white dark:bg-slate-800 border border-cyan-500/30 dark:border-purple-500/30 rounded-lg text-sm text-purple-950 dark:text-cyan-50 hover:border-cyan-400 dark:hover:border-purple-400 focus:outline-none focus:ring-2 focus:ring-orange-500 dark:focus:ring-amber-400 transition-colors"
212
270
>
213
-
<option value="daily">Weekly - Check every week for new matches</option>
214
-
<option value="weekly">Monthly - Check once per month</option>
215
-
<option value="monthly">Quarterly - Check once per quarter</option>
271
+
<option value="daily">Check daily</option>
272
+
<option value="weekly">Check weekly</option>
273
+
<option value="monthly">Check monthly</option>
216
274
</select>
217
275
</div>
218
276
)}
219
277
</div>
220
278
221
-
{!userSettings.saveData && (
222
-
<div className="p-3 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700">
223
-
<p className="text-sm text-gray-600 dark:text-gray-400">
224
-
💡 Enable "Save my data" to use automation features
225
-
</p>
226
-
</div>
227
-
)}
228
-
</div>
229
-
</div>
230
-
231
-
{/* Data Management */}
232
-
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6 border-2 border-slate-200 dark:border-slate-700">
233
-
<div className="flex items-center space-x-3 mb-4">
234
-
<div className="w-12 h-12 bg-gradient-to-br from-gray-400 to-gray-600 rounded-xl flex items-center justify-center shadow-md">
235
-
<Download className="w-6 h-6 text-white" />
236
-
</div>
237
-
<div>
238
-
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">Data Management</h2>
239
-
<p className="text-sm text-gray-600 dark:text-gray-400">Export or reset your settings</p>
240
-
</div>
241
-
</div>
242
-
243
-
<div className="space-y-3">
244
-
<button
279
+
{/* Export Settings Button */}
280
+
{/*<button
245
281
onClick={handleExportSettings}
246
-
className="w-full p-4 bg-gray-50 dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 hover:border-firefly-cyan hover:bg-gray-100 dark:hover:bg-gray-800 transition-all text-left"
282
+
className="w-full flex items-start space-x-4 p-4 bg-purple-100/20 dark:bg-slate-900/50 hover:bg-purple-100/40 dark:hover:bg-slate-900/70 rounded-xl transition-all text-left border-2 border-orange-650/30 dark:border-amber-400/30 hover:border-orange-500 dark:hover:border-orange-400 shadow-md hover:shadow-lg"
247
283
>
248
-
<div className="flex items-center justify-between">
249
-
<div>
250
-
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1">Export Settings</h3>
251
-
<p className="text-sm text-gray-600 dark:text-gray-400">
252
-
Download your settings as a JSON file
253
-
</p>
284
+
<div className="w-12 h-12 bg-gradient-to-r from-gray-400 to-gray-600 rounded-xl flex items-center justify-center flex-shrink-0 shadow-md">
285
+
<Download className="w-6 h-6 text-white" />
286
+
</div>
287
+
<div className="flex-1 min-w-0">
288
+
<div className="font-semibold text-purple-950 dark:text-cyan-50 leading-tight mb-1">
289
+
Export Settings
254
290
</div>
255
-
<Download className="w-5 h-5 text-gray-400 flex-shrink-0" />
291
+
<p className="text-sm text-purple-750 dark:text-cyan-250 leading-tight">
292
+
Download your settings as a JSON file
293
+
</p>
256
294
</div>
257
-
</button>
295
+
</button>*/}
258
296
259
-
<button
297
+
{/* Delete Data Button */}
298
+
{/*<button
260
299
onClick={handleResetSettings}
261
-
className="w-full p-4 bg-red-50 dark:bg-red-900/20 rounded-xl border border-red-200 dark:border-red-800 hover:border-red-400 hover:bg-red-100 dark:hover:bg-red-900/30 transition-all text-left"
300
+
className="w-full flex items-start space-x-4 p-4 bg-red-50/50 dark:bg-red-900/20 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-xl transition-all text-left border-2 border-red-200/50 dark:border-red-800/50 hover:border-red-400 dark:hover:border-red-600 shadow-md hover:shadow-lg"
262
301
>
263
-
<div className="flex items-center justify-between">
264
-
<div>
265
-
<h3 className="font-semibold text-red-700 dark:text-red-400 mb-1">Reset All Settings</h3>
266
-
<p className="text-sm text-red-600 dark:text-red-300">
267
-
Restore all settings to default values
268
-
</p>
302
+
<div className="w-12 h-12 bg-gradient-to-r from-red-500 to-red-600 rounded-xl flex items-center justify-center flex-shrink-0 shadow-md">
303
+
<Trash2 className="w-6 h-6 text-white" />
304
+
</div>
305
+
<div className="flex-1 min-w-0">
306
+
<div className="font-semibold text-red-700 dark:text-red-400 leading-tight mb-1">
307
+
Reset All Settings
269
308
</div>
270
-
<Trash2 className="w-5 h-5 text-red-400 flex-shrink-0" />
309
+
<p className="text-sm text-red-600 dark:text-red-300 leading-tight">
310
+
Restore all settings to default values
311
+
</p>
271
312
</div>
272
-
</button>
273
-
</div>
274
-
</div>
275
-
276
-
{/* Current Configuration Summary */}
277
-
<div className="bg-gradient-to-r from-firefly-cyan/10 via-firefly-orange/10 to-firefly-pink/10 dark:from-firefly-cyan/5 dark:via-firefly-orange/5 dark:to-firefly-pink/5 rounded-2xl p-6 border-2 border-firefly-orange/30">
278
-
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-3">Current Configuration</h3>
279
-
<div className="grid md:grid-cols-3 gap-4 text-sm">
280
-
<div>
281
-
<div className="text-gray-600 dark:text-gray-400 mb-1">Data Storage</div>
282
-
<div className="font-medium text-gray-900 dark:text-gray-100">
283
-
{userSettings.saveData ? '✅ Enabled' : '❌ Disabled'}
284
-
</div>
285
-
</div>
286
-
<div>
287
-
<div className="text-gray-600 dark:text-gray-400 mb-1">Automation</div>
288
-
<div className="font-medium text-gray-900 dark:text-gray-100">
289
-
{userSettings.enableAutomation ? `✅ ${userSettings.automationFrequency}` : '❌ Disabled'}
290
-
</div>
291
-
</div>
292
-
<div>
293
-
<div className="text-gray-600 dark:text-gray-400 mb-1">Wizard</div>
294
-
<div className="font-medium text-gray-900 dark:text-gray-100">
295
-
{userSettings.wizardCompleted ? '✅ Completed' : '⏳ Pending'}
296
-
</div>
297
-
</div>
313
+
</button>*/}
298
314
</div>
299
315
</div>
300
316
</div>
301
317
);
302
-
}
318
+
}
+12
-12
src/types/settings.ts
+12
-12
src/types/settings.ts
···
1
-
export type AtprotoAppId = 'bluesky' | 'tangled' | 'spark' | 'bsky list';
1
+
export type AtprotoAppId = "bluesky" | "tangled" | "spark" | "bsky list";
2
2
3
3
export interface AtprotoApp {
4
4
id: AtprotoAppId;
5
5
name: string;
6
6
description: string;
7
-
color: string;
7
+
link: string;
8
8
icon: string;
9
9
action: string;
10
10
enabled: boolean;
···
24
24
platformDestinations: PlatformDestinations;
25
25
saveData: boolean;
26
26
enableAutomation: boolean;
27
-
automationFrequency: 'weekly' | 'monthly' | 'quarterly';
27
+
automationFrequency: "Weekly" | "Monthly" | "Quarterly";
28
28
wizardCompleted: boolean;
29
29
}
30
30
31
31
export const DEFAULT_SETTINGS: UserSettings = {
32
32
platformDestinations: {
33
-
twitter: 'bluesky',
34
-
instagram: 'bluesky',
35
-
tiktok: 'spark',
36
-
github: 'tangled',
37
-
twitch: 'bluesky',
38
-
youtube: 'bluesky',
39
-
tumblr: 'bluesky',
33
+
twitter: "bluesky",
34
+
instagram: "bluesky",
35
+
tiktok: "spark",
36
+
github: "tangled",
37
+
twitch: "bluesky",
38
+
youtube: "spark",
39
+
tumblr: "bluesky",
40
40
},
41
41
saveData: true,
42
42
enableAutomation: false,
43
-
automationFrequency: 'monthly',
43
+
automationFrequency: "Monthly",
44
44
wizardCompleted: false,
45
-
};
45
+
};
+6
-9
tailwind.config.js
+6
-9
tailwind.config.js
···
6
6
extend: {
7
7
colors: {
8
8
firefly: {
9
-
glow: "#FCD34D", // close to amber-300
10
-
amber: "#F59E0B", // close to amber-500
11
-
orange: "#F97316", // close to orange-500
12
-
pink: "#EC4899", // close to tailwind pink-500
13
-
cyan: "#10D2F4", // close to tailwind cyan-300
9
+
glow: "#FCD34D",
10
+
amber: "#F59E0B",
11
+
orange: "#F97316",
12
+
pink: "#EC4899",
13
+
cyan: "#10D2F4",
14
14
},
15
15
cyan: { 250: "#72EEFD" },
16
16
purple: { 750: "#6A1DD1" },
17
-
orange: { 650: "#DF3F00" },
18
17
yellow: { 650: "#C56508" },
19
-
orange: { 650: "#F26611" },
18
+
orange: { 650: "#DF3F00" },
20
19
pink: { 650: "#CD206A" },
21
20
},
22
21
backgroundImage: ({ theme }) => ({
···
41
40
transform: "translate(15px, -25px) scale(1.1)",
42
41
opacity: "0.9",
43
42
},
44
-
}
45
43
},
46
44
},
47
45
},
48
46
},
49
-
plugins: [],
50
47
};