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