+20
-3
src/App.tsx
+20
-3
src/App.tsx
···
1
-
import { useState, useRef } from "react";
1
+
import { useState, useRef, useEffect } from "react";
2
2
import { ArrowRight } from "lucide-react";
3
3
import LoginPage from "./pages/Login";
4
4
import HomePage from "./pages/Home";
···
10
10
import { useFollow } from "./hooks/useFollows";
11
11
import { useFileUpload } from "./hooks/useFileUpload";
12
12
import { useTheme } from "./hooks/useTheme";
13
-
import ThemeControls from "./components/ThemeControls";
14
13
import Firefly from "./components/Firefly";
15
-
14
+
import { DEFAULT_SETTINGS } from "./types/settings";
15
+
import type { UserSettings } from "./types/settings";
16
16
17
17
export default function App() {
18
18
// Auth hook
···
32
32
// Add state to track current platform
33
33
const [currentPlatform, setCurrentPlatform] = useState<string>('tiktok');
34
34
const saveCalledRef = useRef<string | null>(null); // Track by uploadId
35
+
36
+
// Settings state
37
+
const [userSettings, setUserSettings] = useState<UserSettings>(() => {
38
+
const saved = localStorage.getItem('atlast_settings');
39
+
return saved ? JSON.parse(saved) : DEFAULT_SETTINGS;
40
+
});
41
+
42
+
// Save settings to localStorage whenever they change
43
+
useEffect(() => {
44
+
localStorage.setItem('atlast_settings', JSON.stringify(userSettings));
45
+
}, [userSettings]);
46
+
47
+
const handleSettingsUpdate = (newSettings: Partial<UserSettings>) => {
48
+
setUserSettings(prev => ({ ...prev, ...newSettings }));
49
+
};
35
50
36
51
// Search hook
37
52
const {
···
233
248
isDark={isDark}
234
249
onToggleTheme={toggleTheme}
235
250
onToggleMotion={toggleMotion}
251
+
userSettings={userSettings}
252
+
onSettingsUpdate={handleSettingsUpdate}
236
253
/>
237
254
)}
238
255
+273
src/components/SetupWizard.tsx
+273
src/components/SetupWizard.tsx
···
1
+
import { useState } from 'react';
2
+
import { Heart, X, Check, ChevronRight } from 'lucide-react';
3
+
import { PLATFORMS } from '../constants/platforms';
4
+
import { ATPROTO_APPS } from '../constants/atprotoApps';
5
+
import type { UserSettings, PlatformDestinations } from '../types/settings';
6
+
7
+
interface SetupWizardProps {
8
+
isOpen: boolean;
9
+
onClose: () => void;
10
+
onComplete: (settings: Partial<UserSettings>) => void;
11
+
currentSettings: UserSettings;
12
+
}
13
+
14
+
const wizardSteps = [
15
+
{ title: 'Welcome', description: 'Set up your preferences' },
16
+
{ title: 'Platforms', description: 'Choose where to import from' },
17
+
{ title: 'Destinations', description: 'Where should matches go?' },
18
+
{ title: 'Privacy', description: 'Data & automation settings' },
19
+
{ title: 'Ready!', description: 'All set to find your people' },
20
+
];
21
+
22
+
export default function SetupWizard({ isOpen, onClose, onComplete, currentSettings }: SetupWizardProps) {
23
+
const [wizardStep, setWizardStep] = useState(0);
24
+
const [selectedPlatform, setSelectedPlatform] = useState<string | null>(null);
25
+
const [platformDestinations, setPlatformDestinations] = useState<PlatformDestinations>(
26
+
currentSettings.platformDestinations
27
+
);
28
+
const [saveData, setSaveData] = useState(currentSettings.saveData);
29
+
const [enableAutomation, setEnableAutomation] = useState(currentSettings.enableAutomation);
30
+
const [automationFrequency, setAutomationFrequency] = useState(currentSettings.automationFrequency);
31
+
32
+
if (!isOpen) return null;
33
+
34
+
const handleComplete = () => {
35
+
onComplete({
36
+
platformDestinations,
37
+
saveData,
38
+
enableAutomation,
39
+
automationFrequency,
40
+
wizardCompleted: true,
41
+
});
42
+
onClose();
43
+
};
44
+
45
+
return (
46
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
47
+
<div className="bg-white dark:bg-gray-800 rounded-2xl max-w-2xl w-full shadow-2xl max-h-[90vh] overflow-y-auto">
48
+
{/* Header */}
49
+
<div className="p-6 border-b border-gray-200 dark:border-gray-700 sticky top-0 bg-white dark:bg-gray-800 z-10">
50
+
<div className="flex items-center justify-between mb-4">
51
+
<div className="flex items-center space-x-3">
52
+
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl flex items-center justify-center">
53
+
<Heart className="w-5 h-5 text-white" />
54
+
</div>
55
+
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Setup Assistant</h2>
56
+
</div>
57
+
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
58
+
<X className="w-6 h-6" />
59
+
</button>
60
+
</div>
61
+
{/* Progress */}
62
+
<div className="flex items-center space-x-2">
63
+
{wizardSteps.map((step, idx) => (
64
+
<div key={idx} className="flex-1">
65
+
<div
66
+
className={`h-2 rounded-full transition-all ${
67
+
idx <= wizardStep ? 'bg-gradient-to-r from-blue-500 to-purple-600' : 'bg-gray-200 dark:bg-gray-700'
68
+
}`}
69
+
/>
70
+
</div>
71
+
))}
72
+
</div>
73
+
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
74
+
Step {wizardStep + 1} of {wizardSteps.length}: {wizardSteps[wizardStep].title}
75
+
</div>
76
+
</div>
77
+
78
+
{/* Content */}
79
+
<div className="p-6 min-h-[300px]">
80
+
{wizardStep === 0 && (
81
+
<div className="text-center space-y-4">
82
+
<div className="text-6xl mb-4">👋</div>
83
+
<h3 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Welcome to ATlast!</h3>
84
+
<p className="text-gray-600 dark:text-gray-400 max-w-md mx-auto">
85
+
Let's get you set up in just a few steps. We'll help you configure how you want to reconnect with your
86
+
community on the ATmosphere.
87
+
</p>
88
+
</div>
89
+
)}
90
+
91
+
{wizardStep === 1 && (
92
+
<div className="space-y-4">
93
+
<h3 className="text-xl font-bold text-gray-900 dark:text-gray-100">Which platforms will you import from?</h3>
94
+
<p className="text-sm text-gray-600 dark:text-gray-400">
95
+
Select the platforms you follow people on. We'll help you find them on the ATmosphere.
96
+
</p>
97
+
<div className="grid grid-cols-3 gap-3 mt-4">
98
+
{Object.entries(PLATFORMS).map(([key, p]) => {
99
+
const Icon = p.icon;
100
+
return (
101
+
<button
102
+
key={key}
103
+
onClick={() => setSelectedPlatform(key)}
104
+
className={`p-4 rounded-xl border-2 transition-all ${
105
+
selectedPlatform === key
106
+
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
107
+
: 'border-gray-200 dark:border-gray-700 hover:border-blue-300'
108
+
}`}
109
+
>
110
+
<Icon className="w-8 h-8 mx-auto mb-2 text-gray-700 dark:text-gray-300" />
111
+
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">{p.name}</div>
112
+
</button>
113
+
);
114
+
})}
115
+
</div>
116
+
</div>
117
+
)}
118
+
119
+
{wizardStep === 2 && (
120
+
<div className="space-y-4">
121
+
<h3 className="text-xl font-bold text-gray-900 dark:text-gray-100">Where should matches go?</h3>
122
+
<p className="text-sm text-gray-600 dark:text-gray-400">
123
+
Choose which ATmosphere app to use for each platform. You can change this later.
124
+
</p>
125
+
<div className="space-y-3 mt-4">
126
+
{Object.entries(PLATFORMS).map(([key, p]) => {
127
+
const Icon = p.icon;
128
+
return (
129
+
<div key={key} className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-900 rounded-xl">
130
+
<div className="flex items-center space-x-3">
131
+
<Icon className="w-6 h-6 text-gray-700 dark:text-gray-300" />
132
+
<span className="font-medium text-gray-900 dark:text-gray-100">{p.name}</span>
133
+
</div>
134
+
<select
135
+
value={platformDestinations[key as keyof PlatformDestinations]}
136
+
onChange={(e) =>
137
+
setPlatformDestinations({
138
+
...platformDestinations,
139
+
[key]: e.target.value,
140
+
})
141
+
}
142
+
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"
143
+
>
144
+
{Object.values(ATPROTO_APPS).map((app) => (
145
+
<option key={app.id} value={app.id}>
146
+
{app.icon} {app.name}
147
+
</option>
148
+
))}
149
+
</select>
150
+
</div>
151
+
);
152
+
})}
153
+
</div>
154
+
</div>
155
+
)}
156
+
157
+
{wizardStep === 3 && (
158
+
<div className="space-y-6">
159
+
<div>
160
+
<h3 className="text-xl font-bold text-gray-900 dark:text-gray-100 mb-2">Privacy & Automation</h3>
161
+
<p className="text-sm text-gray-600 dark:text-gray-400">Control how your data is used.</p>
162
+
</div>
163
+
164
+
<div className="space-y-4">
165
+
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-xl">
166
+
<div className="flex items-start space-x-3">
167
+
<input
168
+
type="checkbox"
169
+
checked={saveData}
170
+
onChange={(e) => setSaveData(e.target.checked)}
171
+
className="mt-1"
172
+
id="save-data"
173
+
/>
174
+
<div className="flex-1">
175
+
<label htmlFor="save-data" className="font-medium text-gray-900 dark:text-gray-100 cursor-pointer">
176
+
Save my data for future checks
177
+
</label>
178
+
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
179
+
Store your following lists so we can check for new matches later. You can delete anytime.
180
+
</p>
181
+
</div>
182
+
</div>
183
+
</div>
184
+
185
+
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-xl">
186
+
<div className="flex items-start space-x-3">
187
+
<input
188
+
type="checkbox"
189
+
checked={enableAutomation}
190
+
onChange={(e) => setEnableAutomation(e.target.checked)}
191
+
className="mt-1"
192
+
id="enable-automation"
193
+
/>
194
+
<div className="flex-1">
195
+
<label htmlFor="enable-automation" className="font-medium text-gray-900 dark:text-gray-100 cursor-pointer">
196
+
Notify me about new matches
197
+
</label>
198
+
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
199
+
We'll check periodically and DM you when people you follow join the ATmosphere.
200
+
</p>
201
+
{enableAutomation && (
202
+
<select
203
+
value={automationFrequency}
204
+
onChange={(e) => setAutomationFrequency(e.target.value as 'weekly' | 'monthly' | 'quarterly')}
205
+
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"
206
+
>
207
+
<option value="daily">Check daily</option>
208
+
<option value="weekly">Check weekly</option>
209
+
<option value="monthly">Check monthly</option>
210
+
</select>
211
+
)}
212
+
</div>
213
+
</div>
214
+
</div>
215
+
</div>
216
+
</div>
217
+
)}
218
+
219
+
{wizardStep === 4 && (
220
+
<div className="text-center space-y-4">
221
+
<div className="text-6xl mb-4">🎉</div>
222
+
<h3 className="text-2xl font-bold text-gray-900 dark:text-gray-100">You're all set!</h3>
223
+
<p className="text-gray-600 dark:text-gray-400 max-w-md mx-auto">
224
+
Your preferences have been saved. You can change them anytime in Settings.
225
+
</p>
226
+
<div className="bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 rounded-xl p-4 mt-4">
227
+
<h4 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">Quick Summary:</h4>
228
+
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1 text-left max-w-sm mx-auto">
229
+
<li className="flex items-center space-x-2">
230
+
<Check className="w-4 h-4 text-green-500" />
231
+
<span>Data saving: {saveData ? 'Enabled' : 'Disabled'}</span>
232
+
</li>
233
+
<li className="flex items-center space-x-2">
234
+
<Check className="w-4 h-4 text-green-500" />
235
+
<span>Automation: {enableAutomation ? 'Enabled' : 'Disabled'}</span>
236
+
</li>
237
+
<li className="flex items-center space-x-2">
238
+
<Check className="w-4 h-4 text-green-500" />
239
+
<span>Ready to upload your first file!</span>
240
+
</li>
241
+
</ul>
242
+
</div>
243
+
</div>
244
+
)}
245
+
</div>
246
+
247
+
{/* Footer */}
248
+
<div className="p-6 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between sticky bottom-0 bg-white dark:bg-gray-800">
249
+
<button
250
+
onClick={() => wizardStep > 0 && setWizardStep(wizardStep - 1)}
251
+
disabled={wizardStep === 0}
252
+
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"
253
+
>
254
+
Back
255
+
</button>
256
+
<button
257
+
onClick={() => {
258
+
if (wizardStep < wizardSteps.length - 1) {
259
+
setWizardStep(wizardStep + 1);
260
+
} else {
261
+
handleComplete();
262
+
}
263
+
}}
264
+
className="px-6 py-2 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-lg font-medium hover:from-blue-600 hover:to-purple-700 transition-all flex items-center space-x-2"
265
+
>
266
+
<span>{wizardStep === wizardSteps.length - 1 ? 'Get Started' : 'Next'}</span>
267
+
{wizardStep < wizardSteps.length - 1 && <ChevronRight className="w-4 h-4" />}
268
+
</button>
269
+
</div>
270
+
</div>
271
+
</div>
272
+
);
273
+
}
+48
src/constants/atprotoApps.ts
+48
src/constants/atprotoApps.ts
···
1
+
import type { AtprotoApp } from '../types/settings';
2
+
3
+
export const ATPROTO_APPS: Record<string, AtprotoApp> = {
4
+
bluesky: {
5
+
id: 'bluesky',
6
+
name: 'Bluesky',
7
+
description: 'The main ATmosphere social network',
8
+
color: 'blue',
9
+
icon: '🦋',
10
+
action: 'Follow',
11
+
enabled: true,
12
+
},
13
+
tangled: {
14
+
id: 'tangled',
15
+
name: 'Tangled',
16
+
description: 'Alternative following for developers & creators',
17
+
color: 'purple',
18
+
icon: '🐑',
19
+
action: 'Follow',
20
+
enabled: false, // Not yet integrated
21
+
},
22
+
spark: {
23
+
id: 'spark',
24
+
name: 'Spark',
25
+
description: 'Short-form video focused social',
26
+
color: 'orange',
27
+
icon: '✨',
28
+
action: 'Follow',
29
+
enabled: false, // Not yet integrated
30
+
},
31
+
lists: {
32
+
id: 'bsky list',
33
+
name: 'List',
34
+
description: 'Organize into custom Bluesky lists',
35
+
color: 'green',
36
+
icon: '📃',
37
+
action: 'Add to List',
38
+
enabled: false, // Not yet implemented
39
+
},
40
+
};
41
+
42
+
export function getAppById(id: string): AtprotoApp | undefined {
43
+
return ATPROTO_APPS[id];
44
+
}
45
+
46
+
export function getEnabledApps(): AtprotoApp[] {
47
+
return Object.values(ATPROTO_APPS).filter(app => app.enabled);
48
+
}
+7
src/constants/platforms.ts
+7
src/constants/platforms.ts
···
7
7
accentBg: string;
8
8
fileHint: string;
9
9
enabled: boolean;
10
+
defaultApp: string;
10
11
}
11
12
12
13
export const PLATFORMS: Record<string, PlatformConfig> = {
···
17
18
accentBg: 'bg-blue-500',
18
19
fileHint: 'following.txt, data.json, or data.zip',
19
20
enabled: false,
21
+
defaultApp: 'bluesky',
20
22
},
21
23
instagram: {
22
24
name: 'Instagram',
···
25
27
accentBg: 'bg-pink-500',
26
28
fileHint: 'following.html or data ZIP',
27
29
enabled: true,
30
+
defaultApp: 'bluesky',
28
31
},
29
32
tiktok: {
30
33
name: 'TikTok',
···
33
36
accentBg: 'bg-black',
34
37
fileHint: 'Following.txt or data ZIP',
35
38
enabled: true,
39
+
defaultApp: 'spark',
36
40
},
37
41
tumblr: {
38
42
name: 'Tumblr',
···
41
45
accentBg: 'bg-indigo-600',
42
46
fileHint: 'following.csv or data export',
43
47
enabled: false,
48
+
defaultApp: 'bluesky',
44
49
},
45
50
twitch: {
46
51
name: 'Twitch',
···
49
54
accentBg: 'bg-purple-600',
50
55
fileHint: 'following.json or data export',
51
56
enabled: false,
57
+
defaultApp: 'bluesky'
52
58
},
53
59
youtube: {
54
60
name: 'YouTube',
···
57
63
accentBg: 'bg-red-600',
58
64
fileHint: 'subscriptions.csv or Takeout ZIP',
59
65
enabled: false,
66
+
defaultApp: 'bluesky'
60
67
},
61
68
};
62
69
+10
src/index.css
+10
src/index.css
···
16
16
}
17
17
}
18
18
19
+
/* Hide scrollbar but allow scrolling */
20
+
.scrollbar-hide {
21
+
-ms-overflow-style: none;
22
+
scrollbar-width: none;
23
+
}
24
+
25
+
.scrollbar-hide::-webkit-scrollbar {
26
+
display: none;
27
+
}
28
+
19
29
/* Firefly animation keyframes */
20
30
@keyframes float {
21
31
0%, 100% {
+233
-113
src/pages/Home.tsx
+233
-113
src/pages/Home.tsx
···
1
-
import { Upload, History, FileText, Sparkles } from "lucide-react";
1
+
import { Upload, History, Settings, BookOpen, Grid3x3, ChevronRight, Sparkles } from "lucide-react";
2
2
import { useState, useEffect, useRef } from "react";
3
3
import AppHeader from "../components/AppHeader";
4
4
import PlatformSelector from "../components/PlatformSelector";
5
+
import SetupWizard from "../components/SetupWizard";
5
6
import { apiClient } from "../lib/apiClient";
7
+
import { ATPROTO_APPS } from "../constants/atprotoApps";
6
8
import type { Upload as UploadType } from "../types";
9
+
import type { UserSettings } from "../types/settings";
7
10
8
11
interface atprotoSession {
9
12
did: string;
···
20
23
onFileUpload: (e: React.ChangeEvent<HTMLInputElement>, platform: string) => void;
21
24
onLoadUpload: (uploadId: string) => void;
22
25
currentStep: string;
26
+
userSettings: UserSettings;
27
+
onSettingsUpdate: (settings: Partial<UserSettings>) => void;
28
+
// New props from changes.js
23
29
reducedMotion?: boolean;
24
30
isDark?: boolean;
25
31
onToggleTheme?: () => void;
26
32
onToggleMotion?: () => void;
27
33
}
28
34
35
+
type TabId = 'upload' | 'history' | 'settings' | 'guides' | 'apps';
36
+
29
37
export default function HomePage({
30
38
session,
31
39
onLogout,
···
33
41
onFileUpload,
34
42
onLoadUpload,
35
43
currentStep,
44
+
userSettings,
45
+
onSettingsUpdate,
46
+
// New props
36
47
reducedMotion = false,
37
48
isDark = false,
38
49
onToggleTheme,
39
50
onToggleMotion
40
51
}: HomePageProps) {
52
+
const [activeTab, setActiveTab] = useState<TabId>('upload');
41
53
const [uploads, setUploads] = useState<UploadType[]>([]);
42
54
const [isLoading, setIsLoading] = useState(true);
43
55
const [selectedPlatform, setSelectedPlatform] = useState<string>('');
56
+
const [showWizard, setShowWizard] = useState(false);
44
57
const fileInputRef = useRef<HTMLInputElement>(null);
45
58
46
59
useEffect(() => {
47
60
if (session) {
48
61
loadUploads();
49
62
}
50
-
}, [session]);
63
+
64
+
// Show wizard on first visit
65
+
if (!userSettings.wizardCompleted) {
66
+
setShowWizard(true);
67
+
}
68
+
}, [session, userSettings.wizardCompleted]);
51
69
52
70
async function loadUploads() {
53
71
try {
···
63
81
64
82
const handlePlatformSelect = (platform: string) => {
65
83
setSelectedPlatform(platform);
66
-
// Trigger the file input
67
84
fileInputRef.current?.click();
68
85
};
69
86
···
87
104
return colors[platform] || 'from-gray-400 to-gray-600';
88
105
};
89
106
107
+
const tabs = [
108
+
{ id: 'upload' as TabId, icon: Upload, label: 'Upload' },
109
+
{ id: 'history' as TabId, icon: History, label: 'History' },
110
+
{ id: 'settings' as TabId, icon: Settings, label: 'Settings' },
111
+
{ id: 'guides' as TabId, icon: BookOpen, label: 'Guides' },
112
+
{ id: 'apps' as TabId, icon: Grid3x3, label: 'Apps' },
113
+
];
114
+
90
115
return (
91
-
<div className="min-h-screen">
92
-
<AppHeader
93
-
session={session}
94
-
onLogout={onLogout}
95
-
onNavigate={onNavigate}
96
-
currentStep={currentStep}
97
-
isDark={isDark}
98
-
reducedMotion={reducedMotion}
99
-
onToggleTheme={onToggleTheme}
100
-
onToggleMotion={onToggleMotion}
116
+
// Updated background from changes.js
117
+
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
118
+
<SetupWizard
119
+
isOpen={showWizard}
120
+
onClose={() => setShowWizard(false)}
121
+
onComplete={onSettingsUpdate}
122
+
currentSettings={userSettings}
101
123
/>
102
124
103
-
<div className="max-w-4xl mx-auto px-4 py-8 space-y-6">
104
-
{/* Upload Section */}
105
-
<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">
106
-
<div className="flex items-center space-x-3 mb-4">
107
-
<div
108
-
className={`w-12 h-12 bg-gradient-to-br from-firefly-amber to-firefly-orange rounded-xl flex items-center justify-center shadow-md ${
109
-
reducedMotion ? '' : 'animate-glow-pulse'
110
-
}`}
111
-
>
112
-
<Upload className="w-6 h-6 text-slate-900" />
113
-
</div>
114
-
<div>
115
-
<h2 className="text-xl font-bold text-slate-900 dark:text-slate-100">
116
-
Light Up Your Network
117
-
</h2>
118
-
<p className="text-sm text-slate-700 dark:text-slate-300">
119
-
Upload your data to find your fireflies
120
-
</p>
125
+
{/* Header */}
126
+
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
127
+
{/* Updated AppHeader props from changes.js */}
128
+
<AppHeader
129
+
session={session}
130
+
onLogout={onLogout}
131
+
onNavigate={onNavigate}
132
+
currentStep={currentStep}
133
+
isDark={isDark}
134
+
reducedMotion={reducedMotion}
135
+
onToggleTheme={onToggleTheme}
136
+
onToggleMotion={onToggleMotion}
137
+
/>
138
+
139
+
{/* Tab Navigation */}
140
+
<div className="max-w-6xl mx-auto">
141
+
<div className="overflow-x-auto scrollbar-hide px-4">
142
+
<div className="flex space-x-1 border-b border-gray-200 dark:border-gray-700 min-w-max">
143
+
{tabs.map(tab => {
144
+
const Icon = tab.icon;
145
+
return (
146
+
<button
147
+
key={tab.id}
148
+
onClick={() => setActiveTab(tab.id)}
149
+
className={`flex items-center space-x-2 px-4 py-3 border-b-2 transition-all whitespace-nowrap ${
150
+
activeTab === tab.id
151
+
? 'border-blue-500 text-blue-600 dark:text-blue-400'
152
+
: 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
153
+
}`}
154
+
>
155
+
<Icon className="w-4 h-4" />
156
+
<span className="font-medium">{tab.label}</span>
157
+
</button>
158
+
);
159
+
})}
121
160
</div>
122
161
</div>
123
-
<p className="text-slate-700 dark:text-slate-300 mb-6">
124
-
Click a platform below to upload your exported data and discover matches in the ATmosphere
125
-
</p>
126
-
127
-
<PlatformSelector onPlatformSelect={handlePlatformSelect} />
128
-
129
-
{/* Hidden file input */}
130
-
<input
131
-
id="file-upload"
132
-
ref={fileInputRef}
133
-
type="file"
134
-
accept=".txt,.json,.html,.zip"
135
-
onChange={(e) => onFileUpload(e, selectedPlatform || 'tiktok')}
136
-
className="sr-only"
137
-
aria-label="Upload following data file"
138
-
/>
139
-
140
-
<div className="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-xl border-2 border-blue-200 dark:border-blue-800/30">
141
-
<p className="text-sm text-blue-900 dark:text-blue-300 font-semibold">
142
-
💡 How to get your data:
143
-
</p>
144
-
<p className="text-sm text-blue-900 dark:text-blue-300 mt-2">
145
-
<strong>TikTok:</strong> Profile → Settings → Account → Download your data → Upload Following.txt
146
-
</p>
147
-
<p className="text-sm text-blue-900 dark:text-blue-300 mt-1">
148
-
<strong>Instagram:</strong> Profile → Settings → Accounts Center → Your information and permissions → Download your information → Upload following.html
149
-
</p>
150
-
</div>
151
162
</div>
152
-
153
-
{/* Upload History Section */}
154
-
<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">
155
-
<div className="flex items-center space-x-3 mb-6">
156
-
<Sparkles className="w-6 h-6 text-firefly-amber" />
157
-
<h2 className="text-xl font-bold text-slate-900 dark:text-slate-100">
158
-
Your Light Trail
159
-
</h2>
160
-
</div>
163
+
</div>
161
164
162
-
{isLoading ? (
163
-
<div className="space-y-3">
164
-
{[...Array(3)].map((_, i) => (
165
-
<div key={i} className="animate-pulse flex items-center space-x-4 p-4 bg-slate-50 dark:bg-slate-700 rounded-xl">
166
-
<div className="w-12 h-12 bg-slate-200 dark:bg-slate-600 rounded-xl" />
167
-
<div className="flex-1 space-y-2">
168
-
<div className="h-4 bg-slate-200 dark:bg-slate-600 rounded w-3/4" />
169
-
<div className="h-3 bg-slate-200 dark:bg-slate-600 rounded w-1/2" />
165
+
{/* Tab Content */}
166
+
<div className="max-w-6xl mx-auto px-4 py-8">
167
+
{/* Upload Tab */}
168
+
{activeTab === 'upload' && (
169
+
<div className="space-y-6">
170
+
{/* Setup Assistant */}
171
+
{!userSettings.wizardCompleted && (
172
+
<div className="bg-gradient-to-r from-blue-500 to-purple-600 rounded-2xl p-6 text-white">
173
+
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
174
+
<div className="flex-1">
175
+
<h2 className="text-2xl font-bold mb-2">Need help getting started?</h2>
176
+
<p className="text-white/90">Run the setup assistant to configure your preferences in minutes.</p>
170
177
</div>
178
+
<button
179
+
onClick={() => setShowWizard(true)}
180
+
className="bg-white text-blue-600 px-6 py-3 rounded-xl font-semibold hover:bg-blue-50 transition-all flex items-center space-x-2 whitespace-nowrap"
181
+
>
182
+
<span>Start Setup</span>
183
+
<ChevronRight className="w-4 h-4" />
184
+
</button>
171
185
</div>
172
-
))}
173
-
</div>
174
-
) : uploads.length === 0 ? (
175
-
<div className="text-center py-12">
176
-
<FileText className="w-16 h-16 text-slate-300 dark:text-slate-600 mx-auto mb-4" />
177
-
<p className="text-slate-600 dark:text-slate-400 font-medium">No previous uploads yet</p>
178
-
<p className="text-sm text-slate-500 dark:text-slate-500 mt-2">
179
-
Upload your first file to get started
186
+
</div>
187
+
)}
188
+
189
+
{/* Upload Section */}
190
+
<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">
191
+
<div className="flex items-center space-x-3 mb-4">
192
+
<div
193
+
className={`w-12 h-12 bg-gradient-to-br from-firefly-amber to-firefly-orange rounded-xl flex items-center justify-center shadow-md ${
194
+
reducedMotion ? '' : 'animate-glow-pulse'
195
+
}`}
196
+
>
197
+
<Upload className="w-6 h-6 text-slate-900" />
198
+
</div>
199
+
<div>
200
+
<h2 className="text-xl font-bold text-slate-900 dark:text-slate-100">
201
+
Light Up Your Network
202
+
</h2>
203
+
<p className="text-sm text-slate-700 dark:text-slate-300">
204
+
Upload your data to find your fireflies
205
+
</p>
206
+
</div>
207
+
</div>
208
+
209
+
<p className="text-slate-700 dark:text-slate-300 mb-6">
210
+
Click a platform below to upload your exported data and discover matches in the ATmosphere
180
211
</p>
212
+
213
+
<PlatformSelector onPlatformSelect={handlePlatformSelect} />
214
+
215
+
<input
216
+
id="file-upload"
217
+
ref={fileInputRef}
218
+
type="file"
219
+
accept=".txt,.json,.html,.zip"
220
+
onChange={(e) => onFileUpload(e, selectedPlatform || 'tiktok')}
221
+
className="sr-only"
222
+
aria-label="Upload following data file"
223
+
/>
181
224
</div>
182
-
) : (
183
-
<div className="space-y-3">
184
-
{uploads.map((upload) => (
185
-
<button
186
-
key={upload.uploadId}
187
-
onClick={() => onLoadUpload(upload.uploadId)}
188
-
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"
189
-
>
190
-
<div className={`w-12 h-12 bg-gradient-to-r ${getPlatformColor(upload.sourcePlatform)} rounded-xl flex items-center justify-center flex-shrink-0 shadow-md`}>
191
-
<Sparkles className="w-6 h-6 text-white" />
225
+
</div>
226
+
)}
227
+
228
+
{/* History Tab */}
229
+
{activeTab === 'history' && (
230
+
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
231
+
<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">
232
+
<div className="flex items-center space-x-3 mb-6">
233
+
<Sparkles className="w-6 h-6 text-firefly-amber" />
234
+
<h2 className="text-xl font-bold text-slate-900 dark:text-slate-100">
235
+
Your Light Trail
236
+
</h2>
237
+
</div>
238
+
239
+
{isLoading ? (
240
+
<div className="space-y-3">
241
+
{[...Array(3)].map((_, i) => (
242
+
<div key={i} className="animate-pulse flex items-center space-x-4 p-4 bg-slate-50 dark:bg-slate-700 rounded-xl">
243
+
<div className="w-12 h-12 bg-slate-200 dark:bg-slate-600 rounded-xl" />
244
+
<div className="flex-1 space-y-2">
245
+
<div className="h-4 bg-slate-200 dark:bg-slate-600 rounded w-3/4" />
246
+
<div className="h-3 bg-slate-200 dark:bg-slate-600 rounded w-1/2" />
247
+
</div>
192
248
</div>
193
-
<div className="flex-1 min-w-0">
194
-
<div className="flex flex-wrap items-start justify-between gap-x-4 gap-y-2 mb-1">
195
-
<div className="font-semibold text-slate-900 dark:text-slate-100 capitalize">
196
-
{upload.sourcePlatform}
249
+
))}
250
+
</div>
251
+
) : uploads.length === 0 ? (
252
+
<div className="text-center py-12">
253
+
<Upload className="w-16 h-16 text-slate-300 dark:text-slate-600 mx-auto mb-4" />
254
+
<p className="text-slate-600 dark:text-slate-400 font-medium">No previous uploads yet</p>
255
+
<p className="text-sm text-slate-500 dark:text-slate-500 mt-2">
256
+
Upload your first file to get started
257
+
</p>
258
+
</div>
259
+
) : (
260
+
<div className="space-y-3">
261
+
{uploads.map((upload) => {
262
+
const destApp = ATPROTO_APPS[userSettings.platformDestinations[upload.sourcePlatform as keyof typeof userSettings.platformDestinations]];
263
+
return (
264
+
<button
265
+
key={upload.uploadId}
266
+
onClick={() => onLoadUpload(upload.uploadId)}
267
+
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"
268
+
>
269
+
<div className={`w-12 h-12 bg-gradient-to-r ${getPlatformColor(upload.sourcePlatform)} rounded-xl flex items-center justify-center flex-shrink-0 shadow-md`}>
270
+
<Sparkles className="w-6 h-6 text-white" />
197
271
</div>
198
-
<div className="flex items-center gap-2 flex-shrink-0">
199
-
<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">
200
-
{upload.matchedUsers} {upload.matchedUsers === 1 ? 'firefly' : 'fireflies'}
201
-
</span>
202
-
<div className="text-sm text-slate-600 dark:text-slate-400 font-medium whitespace-nowrap">
203
-
{Math.round((upload.matchedUsers / upload.totalUsers) * 100)}%
272
+
<div className="flex-1 min-w-0">
273
+
<div className="flex flex-wrap items-start justify-between gap-x-4 gap-y-2 mb-1">
274
+
<div className="font-semibold text-slate-900 dark:text-slate-100 capitalize">
275
+
{upload.sourcePlatform}
276
+
</div>
277
+
<div className="flex items-center gap-2 flex-shrink-0">
278
+
<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">
279
+
{upload.matchedUsers} {upload.matchedUsers === 1 ? 'firefly' : 'fireflies'}
280
+
</span>
281
+
<div className="text-sm text-slate-600 dark:text-slate-400 font-medium whitespace-nowrap">
282
+
{Math.round((upload.matchedUsers / upload.totalUsers) * 100)}%
283
+
</div>
284
+
</div>
285
+
</div>
286
+
<div className="text-sm text-slate-700 dark:text-slate-300">
287
+
{upload.totalUsers} users • {formatDate(upload.createdAt)}
204
288
</div>
289
+
{destApp && (
290
+
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
291
+
Sent to {destApp.icon} {destApp.name}
292
+
</div>
293
+
)}
205
294
</div>
206
-
</div>
207
-
<div className="text-sm text-slate-700 dark:text-slate-300">
208
-
{upload.totalUsers} users • {formatDate(upload.createdAt)}
209
-
</div>
210
-
</div>
211
-
</button>
212
-
))}
295
+
</button>
296
+
);
297
+
})}
298
+
</div>
299
+
)}
300
+
</div>
301
+
</div>
302
+
)}
303
+
304
+
{/* Settings Tab - Placeholder */}
305
+
{activeTab === 'settings' && (
306
+
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
307
+
<div className="flex items-center space-x-3 mb-6">
308
+
<Settings className="w-6 h-6 text-gray-600 dark:text-gray-400" />
309
+
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">Settings</h2>
310
+
</div>
311
+
<p className="text-gray-600 dark:text-gray-400">Settings page coming soon...</p>
312
+
</div>
313
+
)}
314
+
315
+
{/* Guides Tab - Placeholder */}
316
+
{activeTab === 'guides' && (
317
+
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
318
+
<div className="flex items-center space-x-3 mb-6">
319
+
<BookOpen className="w-6 h-6 text-gray-600 dark:text-gray-400" />
320
+
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">Platform Guides</h2>
321
+
</div>
322
+
<p className="text-gray-600 dark:text-gray-400">Export guides coming soon...</p>
323
+
</div>
324
+
)}
325
+
326
+
{/* Apps Tab - Placeholder */}
327
+
{activeTab === 'apps' && (
328
+
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
329
+
<div className="flex items-center space-x-3 mb-6">
330
+
<Grid3x3 className="w-6 h-6 text-gray-600 dark:text-gray-400" />
331
+
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">ATmosphere Apps</h2>
213
332
</div>
214
-
)}
215
-
</div>
333
+
<p className="text-gray-600 dark:text-gray-400">Apps directory coming soon...</p>
334
+
</div>
335
+
)}
216
336
</div>
217
337
</div>
218
338
);
+45
src/types/settings.ts
+45
src/types/settings.ts
···
1
+
export type AtprotoAppId = 'bluesky' | 'tangled' | 'spark' | 'bsky list';
2
+
3
+
export interface AtprotoApp {
4
+
id: AtprotoAppId;
5
+
name: string;
6
+
description: string;
7
+
color: string;
8
+
icon: string;
9
+
action: string;
10
+
enabled: boolean;
11
+
}
12
+
13
+
export interface PlatformDestinations {
14
+
twitter: AtprotoAppId;
15
+
instagram: AtprotoAppId;
16
+
tiktok: AtprotoAppId;
17
+
github: AtprotoAppId;
18
+
twitch: AtprotoAppId;
19
+
youtube: AtprotoAppId;
20
+
tumblr: AtprotoAppId;
21
+
}
22
+
23
+
export interface UserSettings {
24
+
platformDestinations: PlatformDestinations;
25
+
saveData: boolean;
26
+
enableAutomation: boolean;
27
+
automationFrequency: 'weekly' | 'monthly' | 'quarterly';
28
+
wizardCompleted: boolean;
29
+
}
30
+
31
+
export const DEFAULT_SETTINGS: UserSettings = {
32
+
platformDestinations: {
33
+
twitter: 'bluesky',
34
+
instagram: 'bluesky',
35
+
tiktok: 'spark',
36
+
github: 'tangled',
37
+
twitch: 'bluesky',
38
+
youtube: 'bluesky',
39
+
tumblr: 'bluesky',
40
+
},
41
+
saveData: true,
42
+
enableAutomation: false,
43
+
automationFrequency: 'monthly',
44
+
wizardCompleted: false,
45
+
};