@myrjfa/state 1.0.8 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +18 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -17
- package/dist/lib/actions/actions.d.ts +170 -140
- package/dist/lib/actions/actions.d.ts.map +1 -1
- package/dist/lib/actions/actions.js +307 -307
- package/dist/lib/actions/chat.d.ts +80 -11
- package/dist/lib/actions/chat.d.ts.map +1 -1
- package/dist/lib/actions/chat.js +81 -20
- package/dist/lib/actions/fetcher.js +84 -84
- package/dist/lib/actions/property.d.ts +77 -0
- package/dist/lib/actions/property.d.ts.map +1 -0
- package/dist/lib/actions/property.js +133 -0
- package/dist/lib/actions/user.d.ts +23 -0
- package/dist/lib/actions/user.d.ts.map +1 -0
- package/dist/lib/actions/user.js +55 -0
- package/dist/lib/authSessionManager.js +34 -34
- package/dist/lib/context/ChatContext.d.ts +8 -1
- package/dist/lib/context/ChatContext.d.ts.map +1 -1
- package/dist/lib/context/ChatContext.js +338 -229
- package/dist/lib/models/chat.d.ts +32 -7
- package/dist/lib/models/chat.d.ts.map +1 -1
- package/dist/lib/models/notfications.d.ts +93 -25
- package/dist/lib/models/notfications.d.ts.map +1 -1
- package/dist/lib/models/notfications.js +55 -41
- package/dist/lib/models/portfolio.d.ts +42 -42
- package/dist/lib/models/property.d.ts +79 -0
- package/dist/lib/models/property.d.ts.map +1 -0
- package/dist/lib/models/property.js +134 -0
- package/dist/lib/models/tile.d.ts +28 -28
- package/dist/lib/userAtom.d.ts +198 -198
- package/dist/lib/userAtom.js +127 -127
- package/dist/lib/utils/fileCompression.d.ts +16 -0
- package/dist/lib/utils/fileCompression.d.ts.map +1 -0
- package/dist/lib/utils/fileCompression.js +56 -0
- package/dist/lib/utils/socialMediaUrl.d.ts +25 -0
- package/dist/lib/utils/socialMediaUrl.d.ts.map +1 -0
- package/dist/lib/utils/socialMediaUrl.js +97 -0
- package/package.json +1 -1
package/dist/lib/userAtom.js
CHANGED
|
@@ -1,127 +1,127 @@
|
|
|
1
|
-
// atoms/userAtom.ts
|
|
2
|
-
import CryptoJS from 'crypto-js';
|
|
3
|
-
import { atom, getDefaultStore } from 'jotai';
|
|
4
|
-
import { atomWithStorage } from 'jotai/utils';
|
|
5
|
-
import { getCurrentUser } from './actions/actions';
|
|
6
|
-
import { validateSession } from './actions/auth';
|
|
7
|
-
const STORAGE_KEY = process.env.NEXT_PUBLIC_USER_KEY;
|
|
8
|
-
const LOGIN_KEY = process.env.NEXT_PUBLIC_LOGIN_KEY;
|
|
9
|
-
const SECRET_KEY = process.env.NEXT_PUBLIC_RACE;
|
|
10
|
-
const EXPIRY_DAYS = 90;
|
|
11
|
-
const EXPIRY_MS = EXPIRY_DAYS * 24 * 60 * 60 * 1000;
|
|
12
|
-
const store = getDefaultStore();
|
|
13
|
-
// 🔐 Encryption
|
|
14
|
-
const encrypt = (data) => CryptoJS.AES.encrypt(JSON.stringify(data), SECRET_KEY).toString();
|
|
15
|
-
const decrypt = (cipher) => {
|
|
16
|
-
try {
|
|
17
|
-
const bytes = CryptoJS.AES.decrypt(cipher, SECRET_KEY);
|
|
18
|
-
const decrypted = bytes.toString(CryptoJS.enc.Utf8);
|
|
19
|
-
return JSON.parse(decrypted);
|
|
20
|
-
}
|
|
21
|
-
catch (e) {
|
|
22
|
-
console.warn('Decryption failed:', e);
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
// 🧠 Wrap expiry in localStorage
|
|
27
|
-
const getWithExpiry = (key) => {
|
|
28
|
-
if (typeof window === 'undefined')
|
|
29
|
-
return null;
|
|
30
|
-
try {
|
|
31
|
-
const raw = localStorage.getItem(key);
|
|
32
|
-
if (!raw)
|
|
33
|
-
return null;
|
|
34
|
-
const data = decrypt(raw);
|
|
35
|
-
if (!data)
|
|
36
|
-
return null;
|
|
37
|
-
const { value, expiry } = data;
|
|
38
|
-
if (Date.now() > expiry) {
|
|
39
|
-
resetAuthState();
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
return value;
|
|
43
|
-
}
|
|
44
|
-
catch (e) {
|
|
45
|
-
console.warn('Invalid user data:', e);
|
|
46
|
-
resetAuthState();
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
const setWithExpiry = (key, value) => {
|
|
51
|
-
if (typeof window === 'undefined')
|
|
52
|
-
return null;
|
|
53
|
-
if (!value)
|
|
54
|
-
return localStorage.removeItem(key);
|
|
55
|
-
const expiry = Date.now() + EXPIRY_MS;
|
|
56
|
-
const encrypted = encrypt({ value, expiry });
|
|
57
|
-
localStorage.setItem(key, encrypted);
|
|
58
|
-
};
|
|
59
|
-
// Storage initialization atom - tracks if we've tried to load from storage
|
|
60
|
-
export const storageInitializedAtom = atom(false);
|
|
61
|
-
// 🍱 Base storage atom (persisted)
|
|
62
|
-
export const userAtom = atomWithStorage(STORAGE_KEY, null, {
|
|
63
|
-
getItem: (key) => getWithExpiry(key),
|
|
64
|
-
setItem: setWithExpiry,
|
|
65
|
-
removeItem: (key) => localStorage.removeItem(key),
|
|
66
|
-
});
|
|
67
|
-
export const isLoggedInAtom = atomWithStorage(LOGIN_KEY, false);
|
|
68
|
-
// 🕒 Tracks if user is loading
|
|
69
|
-
export const userLoadingAtom = atom((get) => {
|
|
70
|
-
// The issue was here - we need to properly track initialization state
|
|
71
|
-
const initialized = get(storageInitializedAtom);
|
|
72
|
-
// We're loading if we haven't initialized storage yet
|
|
73
|
-
return !initialized;
|
|
74
|
-
});
|
|
75
|
-
// Initialize storage atom effect
|
|
76
|
-
export const initializeAuthSessionAtom = atom(null, async (get, set) => {
|
|
77
|
-
// Only run in browser
|
|
78
|
-
if (typeof window === 'undefined')
|
|
79
|
-
return;
|
|
80
|
-
// 1. Validate session (lightweight, no DB)
|
|
81
|
-
const session = await validateSession();
|
|
82
|
-
if (session.success) {
|
|
83
|
-
console.log(session.message);
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
resetAuthState();
|
|
87
|
-
set(storageInitializedAtom, true);
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
// 2. Try cache (localStorage)
|
|
91
|
-
const userData = getWithExpiry(STORAGE_KEY);
|
|
92
|
-
if (userData) {
|
|
93
|
-
set(userAtom, userData);
|
|
94
|
-
set(isLoggedInAtom, true);
|
|
95
|
-
set(storageInitializedAtom, true);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
// 3. If cache miss: fetch user from backend using session cookie
|
|
99
|
-
try {
|
|
100
|
-
// This endpoint can hit DB or another cache as needed
|
|
101
|
-
const remoteUser = await getCurrentUser();
|
|
102
|
-
if (remoteUser) {
|
|
103
|
-
set(userAtom, remoteUser);
|
|
104
|
-
set(isLoggedInAtom, true);
|
|
105
|
-
setWithExpiry(STORAGE_KEY, remoteUser); // Write to local cache for next time
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
resetAuthState();
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
resetAuthState();
|
|
113
|
-
}
|
|
114
|
-
finally {
|
|
115
|
-
set(storageInitializedAtom, true);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
export function resetAuthState() {
|
|
119
|
-
if (typeof window === 'undefined') {
|
|
120
|
-
console.warn('resetAuthState called on server — skipped');
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
localStorage.removeItem(STORAGE_KEY);
|
|
124
|
-
localStorage.removeItem(LOGIN_KEY);
|
|
125
|
-
store.set(userAtom, null);
|
|
126
|
-
store.set(isLoggedInAtom, false);
|
|
127
|
-
}
|
|
1
|
+
// atoms/userAtom.ts
|
|
2
|
+
import CryptoJS from 'crypto-js';
|
|
3
|
+
import { atom, getDefaultStore } from 'jotai';
|
|
4
|
+
import { atomWithStorage } from 'jotai/utils';
|
|
5
|
+
import { getCurrentUser } from './actions/actions';
|
|
6
|
+
import { validateSession } from './actions/auth';
|
|
7
|
+
const STORAGE_KEY = process.env.NEXT_PUBLIC_USER_KEY;
|
|
8
|
+
const LOGIN_KEY = process.env.NEXT_PUBLIC_LOGIN_KEY;
|
|
9
|
+
const SECRET_KEY = process.env.NEXT_PUBLIC_RACE;
|
|
10
|
+
const EXPIRY_DAYS = 90;
|
|
11
|
+
const EXPIRY_MS = EXPIRY_DAYS * 24 * 60 * 60 * 1000;
|
|
12
|
+
const store = getDefaultStore();
|
|
13
|
+
// 🔐 Encryption
|
|
14
|
+
const encrypt = (data) => CryptoJS.AES.encrypt(JSON.stringify(data), SECRET_KEY).toString();
|
|
15
|
+
const decrypt = (cipher) => {
|
|
16
|
+
try {
|
|
17
|
+
const bytes = CryptoJS.AES.decrypt(cipher, SECRET_KEY);
|
|
18
|
+
const decrypted = bytes.toString(CryptoJS.enc.Utf8);
|
|
19
|
+
return JSON.parse(decrypted);
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
console.warn('Decryption failed:', e);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
// 🧠 Wrap expiry in localStorage
|
|
27
|
+
const getWithExpiry = (key) => {
|
|
28
|
+
if (typeof window === 'undefined')
|
|
29
|
+
return null;
|
|
30
|
+
try {
|
|
31
|
+
const raw = localStorage.getItem(key);
|
|
32
|
+
if (!raw)
|
|
33
|
+
return null;
|
|
34
|
+
const data = decrypt(raw);
|
|
35
|
+
if (!data)
|
|
36
|
+
return null;
|
|
37
|
+
const { value, expiry } = data;
|
|
38
|
+
if (Date.now() > expiry) {
|
|
39
|
+
resetAuthState();
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
console.warn('Invalid user data:', e);
|
|
46
|
+
resetAuthState();
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const setWithExpiry = (key, value) => {
|
|
51
|
+
if (typeof window === 'undefined')
|
|
52
|
+
return null;
|
|
53
|
+
if (!value)
|
|
54
|
+
return localStorage.removeItem(key);
|
|
55
|
+
const expiry = Date.now() + EXPIRY_MS;
|
|
56
|
+
const encrypted = encrypt({ value, expiry });
|
|
57
|
+
localStorage.setItem(key, encrypted);
|
|
58
|
+
};
|
|
59
|
+
// Storage initialization atom - tracks if we've tried to load from storage
|
|
60
|
+
export const storageInitializedAtom = atom(false);
|
|
61
|
+
// 🍱 Base storage atom (persisted)
|
|
62
|
+
export const userAtom = atomWithStorage(STORAGE_KEY, null, {
|
|
63
|
+
getItem: (key) => getWithExpiry(key),
|
|
64
|
+
setItem: setWithExpiry,
|
|
65
|
+
removeItem: (key) => localStorage.removeItem(key),
|
|
66
|
+
});
|
|
67
|
+
export const isLoggedInAtom = atomWithStorage(LOGIN_KEY, false);
|
|
68
|
+
// 🕒 Tracks if user is loading
|
|
69
|
+
export const userLoadingAtom = atom((get) => {
|
|
70
|
+
// The issue was here - we need to properly track initialization state
|
|
71
|
+
const initialized = get(storageInitializedAtom);
|
|
72
|
+
// We're loading if we haven't initialized storage yet
|
|
73
|
+
return !initialized;
|
|
74
|
+
});
|
|
75
|
+
// Initialize storage atom effect
|
|
76
|
+
export const initializeAuthSessionAtom = atom(null, async (get, set) => {
|
|
77
|
+
// Only run in browser
|
|
78
|
+
if (typeof window === 'undefined')
|
|
79
|
+
return;
|
|
80
|
+
// 1. Validate session (lightweight, no DB)
|
|
81
|
+
const session = await validateSession();
|
|
82
|
+
if (session.success) {
|
|
83
|
+
console.log(session.message);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
resetAuthState();
|
|
87
|
+
set(storageInitializedAtom, true);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// 2. Try cache (localStorage)
|
|
91
|
+
const userData = getWithExpiry(STORAGE_KEY);
|
|
92
|
+
if (userData) {
|
|
93
|
+
set(userAtom, userData);
|
|
94
|
+
set(isLoggedInAtom, true);
|
|
95
|
+
set(storageInitializedAtom, true);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// 3. If cache miss: fetch user from backend using session cookie
|
|
99
|
+
try {
|
|
100
|
+
// This endpoint can hit DB or another cache as needed
|
|
101
|
+
const remoteUser = await getCurrentUser();
|
|
102
|
+
if (remoteUser) {
|
|
103
|
+
set(userAtom, remoteUser);
|
|
104
|
+
set(isLoggedInAtom, true);
|
|
105
|
+
setWithExpiry(STORAGE_KEY, remoteUser); // Write to local cache for next time
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
resetAuthState();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
resetAuthState();
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
set(storageInitializedAtom, true);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
export function resetAuthState() {
|
|
119
|
+
if (typeof window === 'undefined') {
|
|
120
|
+
console.warn('resetAuthState called on server — skipped');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
124
|
+
localStorage.removeItem(LOGIN_KEY);
|
|
125
|
+
store.set(userAtom, null);
|
|
126
|
+
store.set(isLoggedInAtom, false);
|
|
127
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for client-side file compression before upload.
|
|
3
|
+
*/
|
|
4
|
+
interface CompressionOptions {
|
|
5
|
+
maxWidth?: number;
|
|
6
|
+
maxHeight?: number;
|
|
7
|
+
quality?: number;
|
|
8
|
+
mimeType?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function compressImage(file: File, options?: CompressionOptions): Promise<File>;
|
|
11
|
+
/**
|
|
12
|
+
* Checks if a file is an image that can be compressed.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isCompressibleImage(file: File): boolean;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=fileCompression.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileCompression.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/fileCompression.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,UAAU,kBAAkB;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+D/F;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAEvD"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for client-side file compression before upload.
|
|
3
|
+
*/
|
|
4
|
+
export async function compressImage(file, options = {}) {
|
|
5
|
+
const { maxWidth = 1920, maxHeight = 1080, quality = 0.2, mimeType = "image/jpeg" } = options;
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const img = new Image();
|
|
8
|
+
img.src = URL.createObjectURL(file);
|
|
9
|
+
img.onload = () => {
|
|
10
|
+
URL.revokeObjectURL(img.src);
|
|
11
|
+
const canvas = document.createElement("canvas");
|
|
12
|
+
let width = img.width;
|
|
13
|
+
let height = img.height;
|
|
14
|
+
// Maintain aspect ratio
|
|
15
|
+
if (width > height) {
|
|
16
|
+
if (width > maxWidth) {
|
|
17
|
+
height *= maxWidth / width;
|
|
18
|
+
width = maxWidth;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
if (height > maxHeight) {
|
|
23
|
+
width *= maxHeight / height;
|
|
24
|
+
height = maxHeight;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
canvas.width = width;
|
|
28
|
+
canvas.height = height;
|
|
29
|
+
const ctx = canvas.getContext("2d");
|
|
30
|
+
if (!ctx) {
|
|
31
|
+
return reject(new Error("Could not get canvas context"));
|
|
32
|
+
}
|
|
33
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
34
|
+
canvas.toBlob((blob) => {
|
|
35
|
+
if (!blob) {
|
|
36
|
+
return reject(new Error("Canvas toBlob failed"));
|
|
37
|
+
}
|
|
38
|
+
const compressedFile = new File([blob], file.name, {
|
|
39
|
+
type: mimeType,
|
|
40
|
+
lastModified: Date.now(),
|
|
41
|
+
});
|
|
42
|
+
resolve(compressedFile);
|
|
43
|
+
}, mimeType, quality);
|
|
44
|
+
};
|
|
45
|
+
img.onerror = (err) => {
|
|
46
|
+
URL.revokeObjectURL(img.src);
|
|
47
|
+
reject(err);
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Checks if a file is an image that can be compressed.
|
|
53
|
+
*/
|
|
54
|
+
export function isCompressibleImage(file) {
|
|
55
|
+
return file.type.startsWith("image/") && !file.type.includes("svg") && !file.type.includes("gif");
|
|
56
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes social media input (username or partial URL) into a fully qualified URL.
|
|
3
|
+
* Handles cases where users enter just a username, handle, or partial URL.
|
|
4
|
+
*/
|
|
5
|
+
export type SocialPlatformType = 'instagram' | 'twitter' | 'linkedin' | 'facebook' | 'youtube' | 'github' | 'googleBusiness';
|
|
6
|
+
interface PlatformConfig {
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
usernameRegex: RegExp;
|
|
9
|
+
urlPattern: RegExp;
|
|
10
|
+
buildUrl: (username: string) => string;
|
|
11
|
+
placeholder: string;
|
|
12
|
+
hint: string;
|
|
13
|
+
}
|
|
14
|
+
declare const PLATFORM_CONFIGS: Record<SocialPlatformType, PlatformConfig>;
|
|
15
|
+
/**
|
|
16
|
+
* Attempts to normalize a social media input into a full URL.
|
|
17
|
+
* Returns { url, error } — url is the corrected URL or the original input, error is a message if invalid.
|
|
18
|
+
*/
|
|
19
|
+
export declare function normalizeSocialUrl(platform: SocialPlatformType, input: string): {
|
|
20
|
+
url: string;
|
|
21
|
+
error: string | null;
|
|
22
|
+
};
|
|
23
|
+
export { PLATFORM_CONFIGS };
|
|
24
|
+
export type { SocialPlatformType as SocialPlatform };
|
|
25
|
+
//# sourceMappingURL=socialMediaUrl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"socialMediaUrl.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/socialMediaUrl.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,GAAG,gBAAgB,CAAC;AAE7H,UAAU,cAAc;IACpB,OAAO,EAAE,MAAM,CAAC;IAEhB,aAAa,EAAE,MAAM,CAAC;IAEtB,UAAU,EAAE,MAAM,CAAC;IAEnB,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,QAAA,MAAM,gBAAgB,EAAE,MAAM,CAAC,kBAAkB,EAAE,cAAc,CAyDhE,CAAC;AAEF;;;GAGG;AACH,wBAAgB,kBAAkB,CAC9B,QAAQ,EAAE,kBAAkB,EAC5B,KAAK,EAAE,MAAM,GACd;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAiCvC;AAED,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAC5B,YAAY,EAAE,kBAAkB,IAAI,cAAc,EAAE,CAAC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes social media input (username or partial URL) into a fully qualified URL.
|
|
3
|
+
* Handles cases where users enter just a username, handle, or partial URL.
|
|
4
|
+
*/
|
|
5
|
+
const PLATFORM_CONFIGS = {
|
|
6
|
+
instagram: {
|
|
7
|
+
baseUrl: 'https://instagram.com/',
|
|
8
|
+
usernameRegex: /^[a-zA-Z0-9._]{1,30}$/,
|
|
9
|
+
urlPattern: /^https?:\/\/(www\.)?instagram\.com\/([a-zA-Z0-9._]+)\/?/,
|
|
10
|
+
buildUrl: (u) => `https://instagram.com/${u}`,
|
|
11
|
+
placeholder: 'pankaj.sharma or https://instagram.com/pankaj.sharma',
|
|
12
|
+
hint: 'Username: letters, numbers, . and _ (max 30 chars)',
|
|
13
|
+
},
|
|
14
|
+
twitter: {
|
|
15
|
+
baseUrl: 'https://x.com/',
|
|
16
|
+
usernameRegex: /^[a-zA-Z0-9_]{1,15}$/,
|
|
17
|
+
urlPattern: /^https?:\/\/(www\.)?(twitter\.com|x\.com)\/([a-zA-Z0-9_]+)\/?/,
|
|
18
|
+
buildUrl: (u) => `https://x.com/${u}`,
|
|
19
|
+
placeholder: 'pankajsharma or https://x.com/pankajsharma',
|
|
20
|
+
hint: 'Username: letters, numbers, _ (max 15 chars)',
|
|
21
|
+
},
|
|
22
|
+
linkedin: {
|
|
23
|
+
baseUrl: 'https://linkedin.com/in/',
|
|
24
|
+
usernameRegex: /^[a-zA-Z0-9-]{3,100}$/,
|
|
25
|
+
urlPattern: /^https?:\/\/(www\.)?linkedin\.com\/in\/([a-zA-Z0-9-]+)\/?/,
|
|
26
|
+
buildUrl: (u) => `https://linkedin.com/in/${u}`,
|
|
27
|
+
placeholder: 'pankaj-sharma or https://linkedin.com/in/pankaj-sharma',
|
|
28
|
+
hint: 'Username: letters, numbers, - (3–100 chars)',
|
|
29
|
+
},
|
|
30
|
+
facebook: {
|
|
31
|
+
baseUrl: 'https://facebook.com/',
|
|
32
|
+
usernameRegex: /^[a-zA-Z0-9.]{5,50}$/,
|
|
33
|
+
urlPattern: /^https?:\/\/(www\.)?facebook\.com\/([a-zA-Z0-9.]+)\/?/,
|
|
34
|
+
buildUrl: (u) => `https://facebook.com/${u}`,
|
|
35
|
+
placeholder: 'pankaj.sharma or https://facebook.com/pankaj.sharma',
|
|
36
|
+
hint: 'Username: letters, numbers, . (5–50 chars)',
|
|
37
|
+
},
|
|
38
|
+
youtube: {
|
|
39
|
+
baseUrl: 'https://youtube.com/@',
|
|
40
|
+
usernameRegex: /^@?[a-zA-Z0-9_-]{3,30}$/,
|
|
41
|
+
urlPattern: /^https?:\/\/(www\.)?youtube\.com\/@?([a-zA-Z0-9_-]+)\/?/,
|
|
42
|
+
buildUrl: (u) => `https://youtube.com/@${u.replace(/^@/, '')}`,
|
|
43
|
+
placeholder: '@pankajsharma or https://youtube.com/@pankajsharma',
|
|
44
|
+
hint: 'Handle: letters, numbers, _ and - (3–30 chars)',
|
|
45
|
+
},
|
|
46
|
+
github: {
|
|
47
|
+
baseUrl: 'https://github.com/',
|
|
48
|
+
usernameRegex: /^[a-zA-Z0-9-]{1,39}$/,
|
|
49
|
+
urlPattern: /^https?:\/\/(www\.)?github\.com\/([a-zA-Z0-9-]+)\/?/,
|
|
50
|
+
buildUrl: (u) => `https://github.com/${u}`,
|
|
51
|
+
placeholder: 'pankaj-sharma or https://github.com/pankaj-sharma',
|
|
52
|
+
hint: 'Username: letters, numbers, - (max 39 chars)',
|
|
53
|
+
},
|
|
54
|
+
googleBusiness: {
|
|
55
|
+
baseUrl: 'https://mybusiness.google.com/',
|
|
56
|
+
usernameRegex: /^[a-zA-Z0-9-]{1,39}$/, // Adjust regex as needed
|
|
57
|
+
urlPattern: /^https?:\/\/(www\.)?mybusiness\.google\.com\/([a-zA-Z0-9-]+)\/?/,
|
|
58
|
+
buildUrl: (u) => `https://mybusiness.google.com/${u}`,
|
|
59
|
+
placeholder: 'pankaj-sharma or https://mybusiness.google.com/pankaj-sharma',
|
|
60
|
+
hint: 'Username: letters, numbers, - (max 39 chars)',
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Attempts to normalize a social media input into a full URL.
|
|
65
|
+
* Returns { url, error } — url is the corrected URL or the original input, error is a message if invalid.
|
|
66
|
+
*/
|
|
67
|
+
export function normalizeSocialUrl(platform, input) {
|
|
68
|
+
const cfg = PLATFORM_CONFIGS[platform];
|
|
69
|
+
if (!input || input.trim() === '')
|
|
70
|
+
return { url: '', error: null };
|
|
71
|
+
const trimmed = input.trim();
|
|
72
|
+
// Already a full URL for this platform → extract username and rebuild canonical
|
|
73
|
+
const urlMatch = trimmed.match(cfg.urlPattern);
|
|
74
|
+
if (urlMatch) {
|
|
75
|
+
// Take the captured username group (last capture group)
|
|
76
|
+
const username = urlMatch[urlMatch.length - 1] ?? trimmed;
|
|
77
|
+
return { url: cfg.buildUrl(username), error: null };
|
|
78
|
+
}
|
|
79
|
+
// Looks like a URL for a *different* site or malformed URL
|
|
80
|
+
if (trimmed.startsWith('http://') || trimmed.startsWith('https://') || trimmed.includes('.com/') || trimmed.includes('.net/')) {
|
|
81
|
+
return {
|
|
82
|
+
url: trimmed,
|
|
83
|
+
error: `That doesn't look like a valid ${platform} URL. ${cfg.hint}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// Plain username — validate and build URL
|
|
87
|
+
const usernameToTest = trimmed.replace(/^@/, ''); // strip leading @ for youtube/etc
|
|
88
|
+
const testValue = platform === 'youtube' ? trimmed : usernameToTest;
|
|
89
|
+
if (!cfg.usernameRegex.test(testValue)) {
|
|
90
|
+
return {
|
|
91
|
+
url: trimmed,
|
|
92
|
+
error: `Invalid ${platform} username. ${cfg.hint}`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return { url: cfg.buildUrl(usernameToTest), error: null };
|
|
96
|
+
}
|
|
97
|
+
export { PLATFORM_CONFIGS };
|