+1
-1
netlify/functions/batch-search-actors.ts
+1
-1
netlify/functions/batch-search-actors.ts
···
3
3
import { successResponse } from "./shared/utils";
4
4
import { withAuthErrorHandling } from "./shared/middleware";
5
5
import { ValidationError } from "./shared/constants/errors";
6
+
import { normalize } from "./shared/utils/string.utils";
6
7
7
8
const batchSearchHandler: AuthenticatedHandler = async (context) => {
8
9
// Parse batch request
···
32
33
});
33
34
34
35
// Filter and rank matches
35
-
const normalize = (s: string) => s.toLowerCase().replace(/[._-]/g, "");
36
36
const normalizedUsername = normalize(username);
37
37
38
38
const rankedActors = response.data.actors
+3
-6
netlify/functions/save-results.ts
+3
-6
netlify/functions/save-results.ts
···
5
5
MatchRepository,
6
6
} from "./shared/repositories";
7
7
import { successResponse } from "./shared/utils";
8
+
import { normalize } from "./shared/utils";
8
9
import { withAuthErrorHandling } from "./shared/middleware";
9
10
import { ValidationError } from "./shared/constants/errors";
10
11
···
98
99
// BULK OPERATION 2: Link all users to source accounts
99
100
const links = results
100
101
.map((result) => {
101
-
const normalized = result.sourceUser.username
102
-
.toLowerCase()
103
-
.replace(/[._-]/g, "");
102
+
const normalized = normalize(result.sourceUser.username);
104
103
const sourceAccountId = sourceAccountIdMap.get(normalized);
105
104
return {
106
105
sourceAccountId: sourceAccountId!,
···
127
126
const matchedSourceAccountIds: number[] = [];
128
127
129
128
for (const result of results) {
130
-
const normalized = result.sourceUser.username
131
-
.toLowerCase()
132
-
.replace(/[._-]/g, "");
129
+
const normalized = normalize(result.sourceUser.username);
133
130
const sourceAccountId = sourceAccountIdMap.get(normalized);
134
131
135
132
if (
+1
-9
src/components/HistoryTab.tsx
+1
-9
src/components/HistoryTab.tsx
···
3
3
import type { Upload as UploadType } from "../types";
4
4
import FaviconIcon from "../components/FaviconIcon";
5
5
import type { UserSettings } from "../types/settings";
6
+
import { getPlatformColor } from "../lib/utils/platform";
6
7
7
8
interface HistoryTabProps {
8
9
uploads: UploadType[];
···
30
31
hour: "2-digit",
31
32
minute: "2-digit",
32
33
});
33
-
};
34
-
35
-
const getPlatformColor = (platform: string) => {
36
-
const colors: Record<string, string> = {
37
-
tiktok: "from-black via-gray-800 to-cyan-400",
38
-
twitter: "from-blue-400 to-blue-600",
39
-
instagram: "from-pink-500 via-purple-500 to-orange-500",
40
-
};
41
-
return colors[platform] || "from-gray-400 to-gray-600";
42
34
};
43
35
44
36
return (
+2
-6
src/components/SearchResultCard.tsx
+2
-6
src/components/SearchResultCard.tsx
···
5
5
ChevronDown,
6
6
UserCheck,
7
7
} from "lucide-react";
8
-
import { PLATFORMS } from "../constants/platforms";
9
-
import { ATPROTO_APPS } from "../constants/atprotoApps";
10
8
import type { SearchResult } from "../types";
9
+
import { getPlatform, getAtprotoAppWithFallback } from "../lib/utils/platform";
11
10
import type { AtprotoAppId } from "../types/settings";
12
11
13
12
interface SearchResultCardProps {
···
33
32
? result.atprotoMatches
34
33
: result.atprotoMatches.slice(0, 1);
35
34
const hasMoreMatches = result.atprotoMatches.length > 1;
36
-
const platform = PLATFORMS[sourcePlatform] || PLATFORMS.tiktok;
37
-
38
-
// Get current follow lexicon
39
-
const currentApp = ATPROTO_APPS[destinationAppId];
35
+
const currentApp = getAtprotoAppWithFallback(destinationAppId);
40
36
const currentLexicon = currentApp?.followLexicon || "app.bsky.graph.follow";
41
37
42
38
return (
+3
-8
src/constants/atprotoApps.ts
+3
-8
src/constants/atprotoApps.ts
···
1
1
import type { AtprotoApp } from "../types/settings";
2
2
3
+
// Re-export for convenience
4
+
export type { AtprotoApp } from "../types/settings";
5
+
3
6
export const ATPROTO_APPS: Record<string, AtprotoApp> = {
4
7
bluesky: {
5
8
id: "bluesky",
···
43
46
enabled: false, // Not yet implemented
44
47
},
45
48
};
46
-
47
-
export function getAppById(id: string): AtprotoApp | undefined {
48
-
return ATPROTO_APPS[id];
49
-
}
50
-
51
-
export function getEnabledApps(): AtprotoApp[] {
52
-
return Object.values(ATPROTO_APPS).filter((app) => app.enabled);
53
-
}
+12
src/constants/platforms.ts
+12
src/constants/platforms.ts
···
84
84
export const FOLLOW_CONFIG = {
85
85
BATCH_SIZE: 50,
86
86
};
87
+
88
+
/**
89
+
* @deprecated Use getPlatformColor from lib/utils/platform instead
90
+
**/
91
+
export function getLegacyPlatformColor(platform: string): string {
92
+
const colors: Record<string, string> = {
93
+
tiktok: "from-black via-gray-800 to-cyan-400",
94
+
twitter: "from-blue-400 to-blue-600",
95
+
instagram: "from-pink-500 via-purple-500 to-orange-500",
96
+
};
97
+
return colors[platform] || "from-gray-400 to-gray-600";
98
+
}
+1
-1
src/lib/parserLogic.ts
+1
-1
src/lib/parserLogic.ts
···
5
5
* @param content The string content (HTML or plain text) to search within.
6
6
* @param regexPattern The regex string defining the capture group for the username.
7
7
* @returns An array of extracted usernames.
8
-
*/
8
+
**/
9
9
export function parseTextOrHtml(
10
10
content: string,
11
11
regexPattern: string,
+1
src/lib/utils/index.ts
+1
src/lib/utils/index.ts
···
1
+
export * from "./platform";
+95
src/lib/utils/platform.ts
+95
src/lib/utils/platform.ts
···
1
+
import { PLATFORMS, type PlatformConfig } from "../../constants/platforms";
2
+
import { ATPROTO_APPS, type AtprotoApp } from "../../constants/atprotoApps";
3
+
import type { AtprotoAppId } from "../../types/settings";
4
+
5
+
/**
6
+
* Get platform configuration by key
7
+
*
8
+
* @param platformKey - The platform identifier (e.g., "tiktok", "instagram")
9
+
* @returns Platform configuration or default to TikTok
10
+
**/
11
+
export function getPlatform(platformKey: string): PlatformConfig {
12
+
return PLATFORMS[platformKey] || PLATFORMS.tiktok;
13
+
}
14
+
15
+
/**
16
+
* Get platform gradient color classes for UI
17
+
*
18
+
* @param platformKey - The platform identifier
19
+
* @returns Tailwind gradient classes for the platform
20
+
**/
21
+
export function getPlatformColor(platformKey: string): string {
22
+
const colors: Record<string, string> = {
23
+
tiktok: "from-black via-gray-800 to-cyan-400",
24
+
twitter: "from-blue-400 to-blue-600",
25
+
instagram: "from-pink-500 via-purple-500 to-orange-500",
26
+
tumblr: "from-indigo-600 to-blue-800",
27
+
twitch: "from-purple-600 to-purple-800",
28
+
youtube: "from-red-600 to-red-700",
29
+
};
30
+
return colors[platformKey] || "from-gray-400 to-gray-600";
31
+
}
32
+
33
+
/**
34
+
* Get ATProto app configuration by ID
35
+
*
36
+
* @param appId - The app identifier
37
+
* @returns App configuration or undefined if not found
38
+
**/
39
+
export function getAtprotoApp(appId: AtprotoAppId): AtprotoApp | undefined {
40
+
return ATPROTO_APPS[appId];
41
+
}
42
+
43
+
/**
44
+
* Get ATProto app with fallback to default
45
+
*
46
+
* @param appId - The app identifier
47
+
* @param defaultApp - Default app ID to use as fallback
48
+
* @returns App configuration, falling back to default or Bluesky
49
+
**/
50
+
export function getAtprotoAppWithFallback(
51
+
appId: AtprotoAppId,
52
+
defaultApp: AtprotoAppId = "bluesky",
53
+
): AtprotoApp {
54
+
return (
55
+
ATPROTO_APPS[appId] || ATPROTO_APPS[defaultApp] || ATPROTO_APPS.bluesky
56
+
);
57
+
}
58
+
59
+
/**
60
+
* Get all enabled ATProto apps
61
+
*
62
+
* @returns Array of enabled app configurations
63
+
**/
64
+
export function getEnabledAtprotoApps(): AtprotoApp[] {
65
+
return Object.values(ATPROTO_APPS).filter((app) => app.enabled);
66
+
}
67
+
68
+
/**
69
+
* Get all enabled platforms
70
+
*
71
+
* @returns Array of [key, config] tuples for enabled platforms
72
+
**/
73
+
export function getEnabledPlatforms(): Array<[string, PlatformConfig]> {
74
+
return Object.entries(PLATFORMS).filter(([_, config]) => config.enabled);
75
+
}
76
+
77
+
/**
78
+
* Check if a platform is enabled
79
+
*
80
+
* @param platformKey - The platform identifier
81
+
* @returns True if platform is enabled
82
+
**/
83
+
export function isPlatformEnabled(platformKey: string): boolean {
84
+
return PLATFORMS[platformKey]?.enabled || false;
85
+
}
86
+
87
+
/**
88
+
* Check if an app is enabled
89
+
*
90
+
* @param appId - The app identifier
91
+
* @returns True if app is enabled
92
+
**/
93
+
export function isAppEnabled(appId: AtprotoAppId): boolean {
94
+
return ATPROTO_APPS[appId]?.enabled || false;
95
+
}
+2
-2
src/pages/Loading.tsx
+2
-2
src/pages/Loading.tsx
···
1
1
import AppHeader from "../components/AppHeader";
2
-
import { PLATFORMS } from "../constants/platforms";
2
+
import { getPlatform } from "../lib/utils/platform";
3
3
4
4
interface atprotoSession {
5
5
did: string;
···
40
40
onToggleTheme,
41
41
onToggleMotion,
42
42
}: LoadingPageProps) {
43
-
const platform = PLATFORMS[sourcePlatform] || PLATFORMS.tiktok;
43
+
const platform = getPlatform(sourcePlatform);
44
44
const PlatformIcon = platform.icon;
45
45
46
46
return (
+3
-4
src/pages/Results.tsx
+3
-4
src/pages/Results.tsx
···
1
1
import { Sparkles } from "lucide-react";
2
2
import { useMemo } from "react";
3
-
import { PLATFORMS } from "../constants/platforms";
4
-
import { ATPROTO_APPS } from "../constants/atprotoApps";
5
3
import AppHeader from "../components/AppHeader";
6
4
import SearchResultCard from "../components/SearchResultCard";
7
5
import FaviconIcon from "../components/FaviconIcon";
8
6
import type { AtprotoAppId } from "../types/settings";
7
+
import { getPlatform, getAtprotoApp } from "../lib/utils/platform";
9
8
10
9
interface atprotoSession {
11
10
did: string;
···
74
73
onToggleTheme,
75
74
onToggleMotion,
76
75
}: ResultsPageProps) {
77
-
const platform = PLATFORMS[sourcePlatform] || PLATFORMS.tiktok;
76
+
const platform = getPlatform(sourcePlatform);
77
+
const destinationApp = getAtprotoApp(destinationAppId);
78
78
const PlatformIcon = platform.icon;
79
-
const destinationApp = ATPROTO_APPS[destinationAppId];
80
79
81
80
// Memoize sorted results to avoid re-sorting on every render
82
81
const sortedResults = useMemo(() => {
+32
src/types/auth.types.ts
+32
src/types/auth.types.ts
···
1
+
// Authentication and session types
2
+
3
+
export interface AtprotoSession {
4
+
did: string;
5
+
handle: string;
6
+
displayName?: string;
7
+
avatar?: string;
8
+
description?: string;
9
+
}
10
+
11
+
export interface UserSessionData {
12
+
did: string;
13
+
}
14
+
15
+
export interface OAuthConfig {
16
+
clientId: string;
17
+
redirectUri: string;
18
+
jwksUri?: string;
19
+
clientType: "loopback" | "discoverable";
20
+
usePrivateKey?: boolean;
21
+
}
22
+
23
+
export interface StateData {
24
+
dpopKey: any;
25
+
verifier: string;
26
+
appState?: string;
27
+
}
28
+
29
+
export interface SessionData {
30
+
dpopKey: any;
31
+
tokenSet: any;
32
+
}
+26
src/types/common.types.ts
+26
src/types/common.types.ts
···
1
+
// Common shared types
2
+
3
+
export type AppStep =
4
+
| "checking"
5
+
| "login"
6
+
| "home"
7
+
| "upload"
8
+
| "loading"
9
+
| "results";
10
+
11
+
export interface Upload {
12
+
uploadId: string;
13
+
sourcePlatform: string;
14
+
createdAt: string;
15
+
totalUsers: number;
16
+
matchedUsers: number;
17
+
unmatchedUsers: number;
18
+
}
19
+
20
+
export interface SaveResultsResponse {
21
+
success: boolean;
22
+
uploadId: string;
23
+
totalUsers: number;
24
+
matchedUsers: number;
25
+
unmatchedUsers: number;
26
+
}
+4
-81
src/types/index.ts
+4
-81
src/types/index.ts
···
1
1
// Session and Auth Types
2
-
export interface AtprotoSession {
3
-
did: string;
4
-
handle: string;
5
-
displayName?: string;
6
-
avatar?: string;
7
-
description?: string;
8
-
}
9
-
10
-
// TikTok Data Types
11
-
export interface SourceUser {
12
-
username: string;
13
-
date: string;
14
-
}
2
+
export * from "./auth.types";
15
3
16
4
// Search and Match Types
17
-
export interface AtprotoMatch {
18
-
did: string;
19
-
handle: string;
20
-
displayName?: string;
21
-
avatar?: string;
22
-
matchScore: number;
23
-
description?: string;
24
-
followed?: boolean; // DEPRECATED - kept for backward compatibility
25
-
followStatus?: Record<string, boolean>;
26
-
postCount?: number;
27
-
followerCount?: number;
28
-
foundAt?: string;
29
-
}
5
+
export * from "./search.types";
30
6
31
-
export interface SearchResult {
32
-
sourceUser: SourceUser;
33
-
atprotoMatches: AtprotoMatch[];
34
-
isSearching: boolean;
35
-
error?: string;
36
-
selectedMatches?: Set<string>;
37
-
sourcePlatform: string;
38
-
}
39
-
40
-
// Progress Tracking
41
-
export interface SearchProgress {
42
-
searched: number;
43
-
found: number;
44
-
total: number;
45
-
}
46
-
47
-
// App State
48
-
export type AppStep =
49
-
| "checking"
50
-
| "login"
51
-
| "home"
52
-
| "upload"
53
-
| "loading"
54
-
| "results";
55
-
56
-
// API Response Types
57
-
export interface BatchSearchResult {
58
-
username: string;
59
-
actors: AtprotoMatch[];
60
-
error?: string;
61
-
}
62
-
63
-
export interface BatchFollowResult {
64
-
did: string;
65
-
success: boolean;
66
-
alreadyFollowing?: boolean;
67
-
error: string | null;
68
-
}
69
-
70
-
export interface SaveResultsResponse {
71
-
success: boolean;
72
-
uploadId: string;
73
-
totalUsers: number;
74
-
matchedUsers: number;
75
-
unmatchedUsers: number;
76
-
}
77
-
78
-
export interface Upload {
79
-
uploadId: string;
80
-
sourcePlatform: string;
81
-
createdAt: string;
82
-
totalUsers: number;
83
-
matchedUsers: number;
84
-
unmatchedUsers: number;
85
-
}
7
+
// Common Types
8
+
export * from "./common.types";
86
9
87
10
// Re-export settings types for convenience
88
11
export type {
+46
src/types/search.types.ts
+46
src/types/search.types.ts
···
1
+
export interface SourceUser {
2
+
username: string;
3
+
date: string;
4
+
}
5
+
6
+
export interface AtprotoMatch {
7
+
did: string;
8
+
handle: string;
9
+
displayName?: string;
10
+
avatar?: string;
11
+
matchScore: number;
12
+
description?: string;
13
+
followed?: boolean; // DEPRECATED - kept for backward compatibility
14
+
followStatus?: Record<string, boolean>;
15
+
postCount?: number;
16
+
followerCount?: number;
17
+
foundAt?: string;
18
+
}
19
+
20
+
export interface SearchResult {
21
+
sourceUser: SourceUser;
22
+
atprotoMatches: AtprotoMatch[];
23
+
isSearching: boolean;
24
+
error?: string;
25
+
selectedMatches?: Set<string>;
26
+
sourcePlatform: string;
27
+
}
28
+
29
+
export interface SearchProgress {
30
+
searched: number;
31
+
found: number;
32
+
total: number;
33
+
}
34
+
35
+
export interface BatchSearchResult {
36
+
username: string;
37
+
actors: AtprotoMatch[];
38
+
error?: string;
39
+
}
40
+
41
+
export interface BatchFollowResult {
42
+
did: string;
43
+
success: boolean;
44
+
alreadyFollowing?: boolean;
45
+
error: string | null;
46
+
}