ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto

new dashboard layout

Changed files
+636 -116
src
+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
··· 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
··· 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 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
··· 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
··· 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
··· 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 + };