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

the ultimate GLOW UP

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