@robosystems/core 0.5.6 → 0.5.7
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.
|
@@ -11,5 +11,12 @@ export interface SignUpFormProps {
|
|
|
11
11
|
apiUrl: string;
|
|
12
12
|
currentApp?: string;
|
|
13
13
|
turnstileSiteKey?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Organization invitation token, from the `?invite=` link in the email.
|
|
16
|
+
* When present the form registers the user into the inviting organization
|
|
17
|
+
* instead of creating a personal one, and the invited address is fixed —
|
|
18
|
+
* the token is only valid for that email.
|
|
19
|
+
*/
|
|
20
|
+
inviteToken?: string;
|
|
14
21
|
}
|
|
15
|
-
export declare function SignUpForm({ onSuccess, onRedirect, redirectTo, className, showConfirmPassword, showTermsAcceptance, autoSignIn, apiUrl, currentApp, turnstileSiteKey, }: SignUpFormProps): React.JSX.Element;
|
|
22
|
+
export declare function SignUpForm({ onSuccess, onRedirect, redirectTo, className, showConfirmPassword, showTermsAcceptance, autoSignIn, apiUrl, currentApp, turnstileSiteKey, inviteToken, }: SignUpFormProps): React.JSX.Element;
|
|
@@ -5,7 +5,7 @@ import { RoboSystemsAuthClient } from '../auth-core/client';
|
|
|
5
5
|
import { getAppConfig } from '../auth-core/config';
|
|
6
6
|
import { LogoBadge, Spinner } from '../ui-components';
|
|
7
7
|
import { TurnstileWidget } from './TurnstileWidget';
|
|
8
|
-
export function SignUpForm({ onSuccess, onRedirect, redirectTo = '/login', className = '', showConfirmPassword = true, showTermsAcceptance = true, autoSignIn = false, apiUrl, currentApp, turnstileSiteKey, }) {
|
|
8
|
+
export function SignUpForm({ onSuccess, onRedirect, redirectTo = '/login', className = '', showConfirmPassword = true, showTermsAcceptance = true, autoSignIn = false, apiUrl, currentApp, turnstileSiteKey, inviteToken, }) {
|
|
9
9
|
const [formData, setFormData] = useState({
|
|
10
10
|
name: '',
|
|
11
11
|
email: '',
|
|
@@ -18,8 +18,39 @@ export function SignUpForm({ onSuccess, onRedirect, redirectTo = '/login', class
|
|
|
18
18
|
const [captchaError, setCaptchaError] = useState(null);
|
|
19
19
|
const [passwordStrength, setPasswordStrength] = useState(null);
|
|
20
20
|
const [checkingPassword, setCheckingPassword] = useState(false);
|
|
21
|
+
const [invitation, setInvitation] = useState(null);
|
|
22
|
+
const [inviteChecked, setInviteChecked] = useState(!inviteToken);
|
|
21
23
|
const debounceRef = useRef(null);
|
|
22
24
|
const authClientRef = useRef(new RoboSystemsAuthClient(apiUrl));
|
|
25
|
+
// Resolve the invitation before the form is usable. A token that no longer
|
|
26
|
+
// resolves (revoked, expired, already accepted) degrades to ordinary
|
|
27
|
+
// registration with a notice rather than a dead end.
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!inviteToken)
|
|
30
|
+
return;
|
|
31
|
+
let cancelled = false;
|
|
32
|
+
setInviteChecked(false);
|
|
33
|
+
authClientRef.current
|
|
34
|
+
.getInvitation(inviteToken)
|
|
35
|
+
.then((preview) => {
|
|
36
|
+
if (cancelled)
|
|
37
|
+
return;
|
|
38
|
+
if (preview) {
|
|
39
|
+
setInvitation(preview);
|
|
40
|
+
setFormData((prev) => (Object.assign(Object.assign({}, prev), { email: preview.email })));
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
setError('This invitation link is no longer valid. It may have expired or already been used. You can still create an account below.');
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
.finally(() => {
|
|
47
|
+
if (!cancelled)
|
|
48
|
+
setInviteChecked(true);
|
|
49
|
+
});
|
|
50
|
+
return () => {
|
|
51
|
+
cancelled = true;
|
|
52
|
+
};
|
|
53
|
+
}, [inviteToken]);
|
|
23
54
|
const checkPassword = useCallback(async (password, email) => {
|
|
24
55
|
if (password.length < 4) {
|
|
25
56
|
setPasswordStrength(null);
|
|
@@ -100,7 +131,7 @@ export function SignUpForm({ onSuccess, onRedirect, redirectTo = '/login', class
|
|
|
100
131
|
return;
|
|
101
132
|
}
|
|
102
133
|
try {
|
|
103
|
-
const result = await authClientRef.current.register(formData.email, formData.password, formData.name, captchaToken || undefined);
|
|
134
|
+
const result = await authClientRef.current.register(formData.email, formData.password, formData.name, captchaToken || undefined, invitation ? inviteToken : undefined);
|
|
104
135
|
// Call onSuccess callback if provided
|
|
105
136
|
if (onSuccess) {
|
|
106
137
|
onSuccess(result.user);
|
|
@@ -142,7 +173,12 @@ export function SignUpForm({ onSuccess, onRedirect, redirectTo = '/login', class
|
|
|
142
173
|
};
|
|
143
174
|
// Determine app name based on currentApp prop
|
|
144
175
|
const appName = getAppConfig(currentApp || 'robosystems').displayName;
|
|
145
|
-
return (_jsx("div", { className: "via-primary-950 flex min-h-screen items-center justify-center bg-linear-to-br from-black to-zinc-900 px-4 py-12 sm:px-6 lg:px-8", children: _jsxs("div", { className: "w-full max-w-lg space-y-8", children: [_jsxs("div", { className: "text-center", children: [_jsx(LogoBadge, { animate: "once", className: "mx-auto h-14 w-14" }), _jsx("h1", { className: "font-heading mt-4 text-center text-2xl font-semibold tracking-tight text-white", children: appName }), _jsx("h2", { className: "mt-6 text-center text-xl font-semibold tracking-tight text-gray-300", children: "Create your account" })] }), _jsxs("form", { className: ['mt-8 space-y-6', className].filter(Boolean).join(' '), onSubmit: handleSubmit, children: [error && (_jsx("div", { className: "rounded-md border border-red-800 bg-red-900/50 p-4", children: _jsx("div", { className: "text-sm text-red-300", children: error }) })), captchaError && (_jsx("div", { className: "rounded-md border border-red-800 bg-red-900/50 p-4", children: _jsx("div", { className: "text-sm text-red-300", children: captchaError }) })), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { htmlFor: "name", className: "sr-only", children: "Full name" }), _jsx("input", { id: "name", name: "name", type: "text", autoComplete: "name", required: true, value: formData.name, onChange: handleInputChange, className: "focus:ring-primary-500 relative block w-full rounded-md border-0 bg-gray-800 px-5 py-4 text-base leading-7 text-white ring-1 ring-gray-600 ring-inset placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset", placeholder: "Full name", disabled: loading })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "email", className: "sr-only", children: "Email address" }), _jsx("input", { id: "email", name: "email", type: "email", autoComplete: "email", required: true, value: formData.email, onChange: handleInputChange,
|
|
176
|
+
return (_jsx("div", { className: "via-primary-950 flex min-h-screen items-center justify-center bg-linear-to-br from-black to-zinc-900 px-4 py-12 sm:px-6 lg:px-8", children: _jsxs("div", { className: "w-full max-w-lg space-y-8", children: [_jsxs("div", { className: "text-center", children: [_jsx(LogoBadge, { animate: "once", className: "mx-auto h-14 w-14" }), _jsx("h1", { className: "font-heading mt-4 text-center text-2xl font-semibold tracking-tight text-white", children: appName }), _jsx("h2", { className: "mt-6 text-center text-xl font-semibold tracking-tight text-gray-300", children: invitation ? `Join ${invitation.org_name}` : 'Create your account' }), invitation && (_jsxs("p", { className: "mt-2 text-center text-sm text-gray-400", children: ["You've been invited as", ' ', _jsx("span", { className: "font-medium text-gray-200", children: invitation.role }), ". Create your account to accept."] }))] }), _jsxs("form", { className: ['mt-8 space-y-6', className].filter(Boolean).join(' '), onSubmit: handleSubmit, children: [error && (_jsx("div", { className: "rounded-md border border-red-800 bg-red-900/50 p-4", children: _jsx("div", { className: "text-sm text-red-300", children: error }) })), captchaError && (_jsx("div", { className: "rounded-md border border-red-800 bg-red-900/50 p-4", children: _jsx("div", { className: "text-sm text-red-300", children: captchaError }) })), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { htmlFor: "name", className: "sr-only", children: "Full name" }), _jsx("input", { id: "name", name: "name", type: "text", autoComplete: "name", required: true, value: formData.name, onChange: handleInputChange, className: "focus:ring-primary-500 relative block w-full rounded-md border-0 bg-gray-800 px-5 py-4 text-base leading-7 text-white ring-1 ring-gray-600 ring-inset placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset", placeholder: "Full name", disabled: loading })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "email", className: "sr-only", children: "Email address" }), _jsx("input", { id: "email", name: "email", type: "email", autoComplete: "email", required: true, value: formData.email, onChange: handleInputChange, readOnly: !!invitation, className: [
|
|
177
|
+
'focus:ring-primary-500 relative block w-full rounded-md border-0 bg-gray-800 px-5 py-4 text-base leading-7 text-white ring-1 ring-gray-600 ring-inset placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset',
|
|
178
|
+
invitation ? 'cursor-not-allowed text-gray-300' : '',
|
|
179
|
+
]
|
|
180
|
+
.filter(Boolean)
|
|
181
|
+
.join(' '), placeholder: "Email address", disabled: loading }), invitation && (_jsx("p", { className: "mt-1 text-xs text-gray-400", children: "The invitation was sent to this address and can only be accepted with it." }))] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "password", className: "sr-only", children: "Password" }), _jsx("input", { id: "password", name: "password", type: "password", autoComplete: "new-password", required: true, value: formData.password, onChange: handleInputChange, className: "focus:ring-primary-500 relative block w-full rounded-md border-0 bg-gray-800 px-5 py-4 text-base leading-7 text-white ring-1 ring-gray-600 ring-inset placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset", placeholder: "Password", disabled: loading })] }), showConfirmPassword && (_jsxs("div", { children: [_jsx("label", { htmlFor: "confirmPassword", className: "sr-only", children: "Confirm password" }), _jsx("input", { id: "confirmPassword", name: "confirmPassword", type: "password", autoComplete: "new-password", required: true, value: formData.confirmPassword, onChange: handleInputChange, className: "focus:ring-primary-500 relative block w-full rounded-md border-0 bg-gray-800 px-5 py-4 text-base leading-7 text-white ring-1 ring-gray-600 ring-inset placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset", placeholder: "Confirm password", disabled: loading })] })), _jsx("div", { className: "min-h-[2.75rem]", children: passwordStrength && (_jsxs("div", { className: "space-y-1.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "h-1.5 flex-1 overflow-hidden rounded-full bg-gray-700", children: _jsx("div", { className: [
|
|
146
182
|
'h-full rounded-full transition-all duration-300',
|
|
147
183
|
passwordStrength.score < 30
|
|
148
184
|
? 'bg-red-500'
|
|
@@ -163,5 +199,12 @@ export function SignUpForm({ onSuccess, onRedirect, redirectTo = '/login', class
|
|
|
163
199
|
].join(' '), children: passwordStrength.strength.replace('-', ' ') })] }), passwordStrength.errors.length > 0 && (_jsx("p", { className: "text-xs text-red-400", children: passwordStrength.errors[0] })), passwordStrength.errors.length === 0 &&
|
|
164
200
|
passwordStrength.suggestions.length > 0 && (_jsx("p", { className: "text-xs text-gray-400", children: passwordStrength.suggestions[0] }))] })) })] }), turnstileSiteKey && (_jsx("div", { className: "flex justify-center", children: _jsx(TurnstileWidget, { siteKey: turnstileSiteKey, onVerify: handleCaptchaVerify, onError: handleCaptchaError, onExpire: handleCaptchaExpire, theme: "dark", className: "flex justify-center", disabled: loading }) })), _jsx("div", { children: _jsxs("button", { type: "submit", disabled: loading ||
|
|
165
201
|
checkingPassword ||
|
|
166
|
-
|
|
202
|
+
!inviteChecked ||
|
|
203
|
+
(turnstileSiteKey && !captchaToken), className: "group bg-primary-600 hover:bg-primary-700 focus-visible:outline-primary-500 relative flex w-full justify-center rounded-md px-4 py-3 text-sm font-semibold text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid disabled:cursor-not-allowed disabled:opacity-50", children: [(loading || !inviteChecked) && (_jsx(Spinner, { size: "sm", className: "mr-2 text-white" })), loading
|
|
204
|
+
? 'Creating account...'
|
|
205
|
+
: !inviteChecked
|
|
206
|
+
? 'Checking invitation...'
|
|
207
|
+
: invitation
|
|
208
|
+
? 'Accept invitation'
|
|
209
|
+
: 'Create account'] }) }), showTermsAcceptance && (_jsxs("div", { className: "text-center text-xs text-gray-400", children: ["By creating an account, you agree to our", ' ', _jsx("a", { href: `${process.env.NEXT_PUBLIC_ROBOSYSTEMS_APP_URL || 'https://robosystems.ai'}/pages/terms`, target: "_blank", rel: "noopener noreferrer", className: "hover:text-primary-400 font-medium text-gray-300", children: "Terms of Service" }), ' ', "and", ' ', _jsx("a", { href: `${process.env.NEXT_PUBLIC_ROBOSYSTEMS_APP_URL || 'https://robosystems.ai'}/pages/privacy`, target: "_blank", rel: "noopener noreferrer", className: "hover:text-primary-400 font-medium text-gray-300", children: "Privacy Policy" })] })), _jsx("div", { className: "text-center", children: _jsx("button", { type: "button", onClick: handleSignInClick, className: "hover:text-primary-400 font-medium text-gray-300", children: "Already have an account? Sign in" }) })] })] }) }));
|
|
167
210
|
}
|
package/auth-core/client.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export declare class RoboSystemsAuthClient {
|
|
|
35
35
|
*/
|
|
36
36
|
private syncClientConfigToken;
|
|
37
37
|
login(email: string, password: string): Promise<AuthResponse>;
|
|
38
|
-
register(email: string, password: string, name?: string, captchaToken?: string): Promise<AuthResponse>;
|
|
38
|
+
register(email: string, password: string, name?: string, captchaToken?: string, inviteToken?: string): Promise<AuthResponse>;
|
|
39
39
|
logout(): Promise<void>;
|
|
40
40
|
getCurrentUser(): Promise<AuthUser>;
|
|
41
41
|
private performAuthCheck;
|
|
@@ -105,6 +105,20 @@ export declare class RoboSystemsAuthClient {
|
|
|
105
105
|
/**
|
|
106
106
|
* Check password strength
|
|
107
107
|
*/
|
|
108
|
+
/**
|
|
109
|
+
* Look up an organization invitation by its token.
|
|
110
|
+
*
|
|
111
|
+
* Unauthenticated by design — the token is the credential, and the sign-up
|
|
112
|
+
* page has to render who invited you before an account exists. Returns null
|
|
113
|
+
* for a token that is unknown, revoked, expired, or already accepted, so the
|
|
114
|
+
* caller can fall back to ordinary registration rather than dead-ending.
|
|
115
|
+
*/
|
|
116
|
+
getInvitation(token: string): Promise<{
|
|
117
|
+
org_name: string;
|
|
118
|
+
email: string;
|
|
119
|
+
role: string;
|
|
120
|
+
expires_at: string;
|
|
121
|
+
} | null>;
|
|
108
122
|
checkPasswordStrength(password: string, email?: string): Promise<{
|
|
109
123
|
score: number;
|
|
110
124
|
strength: 'very-weak' | 'weak' | 'fair' | 'good' | 'strong';
|
package/auth-core/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
var _a;
|
|
2
|
-
import { checkPasswordStrength, client, completeSsoAuth, createUserApiKey, forgotPassword, generateSsoToken, getCurrentAuthUser, getPasswordPolicy, listUserApiKeys, loginUser, logoutUser, refreshAuthSession, registerUser, resendVerificationEmail, resetPassword, revokeUserApiKey, ssoTokenExchange, validateResetToken, verifyEmail, } from '@robosystems/client';
|
|
2
|
+
import { checkPasswordStrength, client, completeSsoAuth, createUserApiKey, forgotPassword, generateSsoToken, getCurrentAuthUser, getInvitationPreview, getPasswordPolicy, listUserApiKeys, loginUser, logoutUser, refreshAuthSession, registerUser, resendVerificationEmail, resetPassword, revokeUserApiKey, ssoTokenExchange, validateResetToken, verifyEmail, } from '@robosystems/client';
|
|
3
3
|
import * as sdkClientsModule from '@robosystems/client/clients';
|
|
4
4
|
import { getToken, getValidToken } from './token-storage';
|
|
5
5
|
// Global config hook for the clients package's lazy default singleton
|
|
@@ -200,7 +200,7 @@ export class RoboSystemsAuthClient {
|
|
|
200
200
|
refresh_threshold: sdkResponse.refresh_threshold,
|
|
201
201
|
};
|
|
202
202
|
}
|
|
203
|
-
async register(email, password, name, captchaToken) {
|
|
203
|
+
async register(email, password, name, captchaToken, inviteToken) {
|
|
204
204
|
const response = await registerUser({
|
|
205
205
|
client: this.client,
|
|
206
206
|
body: {
|
|
@@ -208,6 +208,7 @@ export class RoboSystemsAuthClient {
|
|
|
208
208
|
password,
|
|
209
209
|
name: name || '',
|
|
210
210
|
captcha_token: captchaToken || undefined,
|
|
211
|
+
invite_token: inviteToken || undefined,
|
|
211
212
|
},
|
|
212
213
|
});
|
|
213
214
|
// Check for error responses (4xx/5xx)
|
|
@@ -668,6 +669,30 @@ export class RoboSystemsAuthClient {
|
|
|
668
669
|
/**
|
|
669
670
|
* Check password strength
|
|
670
671
|
*/
|
|
672
|
+
/**
|
|
673
|
+
* Look up an organization invitation by its token.
|
|
674
|
+
*
|
|
675
|
+
* Unauthenticated by design — the token is the credential, and the sign-up
|
|
676
|
+
* page has to render who invited you before an account exists. Returns null
|
|
677
|
+
* for a token that is unknown, revoked, expired, or already accepted, so the
|
|
678
|
+
* caller can fall back to ordinary registration rather than dead-ending.
|
|
679
|
+
*/
|
|
680
|
+
async getInvitation(token) {
|
|
681
|
+
try {
|
|
682
|
+
const response = await getInvitationPreview({
|
|
683
|
+
client: this.client,
|
|
684
|
+
path: { token },
|
|
685
|
+
});
|
|
686
|
+
if (response.error || !response.data) {
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
const data = response.data;
|
|
690
|
+
return data;
|
|
691
|
+
}
|
|
692
|
+
catch (_a) {
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
671
696
|
async checkPasswordStrength(password, email) {
|
|
672
697
|
try {
|
|
673
698
|
const response = await checkPasswordStrength({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robosystems/core",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "Shared RoboSystems frontend core library (auth, contexts, task monitoring, UI components) for the RoboSystems Next.js apps",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"tailwind-merge": "^3"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@robosystems/client": ">=0.
|
|
30
|
+
"@robosystems/client": ">=0.6.2 <1",
|
|
31
31
|
"flowbite-react": "^0.12.5",
|
|
32
32
|
"next": ">=15 <17",
|
|
33
33
|
"react": ">=18 <20",
|
|
@@ -24,11 +24,11 @@ export function LandingFooter({ tagline, productLinks, contactModal: ContactModa
|
|
|
24
24
|
const companyLinkProps = isRoboSystems
|
|
25
25
|
? {}
|
|
26
26
|
: { target: '_blank', rel: 'noopener noreferrer' };
|
|
27
|
-
return (_jsxs("footer", { className: "border-t border-gray-800 bg-zinc-950", children: [_jsxs("div", { className: "mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8", children: [_jsxs("div", { className: "grid gap-4 md:grid-cols-5 lg:gap-6", children: [_jsxs("div", { className: "md:col-span-2", children: [_jsxs("div", { className: "mb-4 flex items-center gap-2", children: [_jsx(Image, { src: `/images/logos/${CURRENT_APP}.png`, alt: current.displayName, width: 32, height: 32, className: "rounded-lg" }), _jsx("span", { className: "font-heading text-xl font-semibold text-white", children: current.displayName })] }), _jsx("p", { className: "mb-4 max-w-md text-sm text-gray-400", children: tagline }), _jsxs("div", { className: "flex gap-4", children: [_jsx(Link, { href: "https://github.com/RoboFinSystems", target: "_blank", rel: "noopener noreferrer", "aria-label": "RoboSystems on GitHub", className: linkClass, children: _jsx("svg", { className: "h-6 w-6", fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" }) }) }), _jsx(Link, { href: "https://x.com/robofinsystems", target: "_blank", rel: "noopener noreferrer", "aria-label": "RoboSystems on X", className: linkClass, children: _jsx("svg", { className: "h-6 w-6", fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z" }) }) }), _jsx(Link, { href: "https://www.linkedin.com/company/
|
|
27
|
+
return (_jsxs("footer", { className: "border-t border-gray-800 bg-zinc-950", children: [_jsxs("div", { className: "mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8", children: [_jsxs("div", { className: "grid gap-4 md:grid-cols-5 lg:gap-6", children: [_jsxs("div", { className: "md:col-span-2", children: [_jsxs("div", { className: "mb-4 flex items-center gap-2", children: [_jsx(Image, { src: `/images/logos/${CURRENT_APP}.png`, alt: current.displayName, width: 32, height: 32, className: "rounded-lg" }), _jsx("span", { className: "font-heading text-xl font-semibold text-white", children: current.displayName })] }), _jsx("p", { className: "mb-4 max-w-md text-sm text-gray-400", children: tagline }), _jsxs("div", { className: "flex gap-4", children: [_jsx(Link, { href: "https://github.com/RoboFinSystems", target: "_blank", rel: "noopener noreferrer", "aria-label": "RoboSystems on GitHub", className: linkClass, children: _jsx("svg", { className: "h-6 w-6", fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" }) }) }), _jsx(Link, { href: "https://x.com/robofinsystems", target: "_blank", rel: "noopener noreferrer", "aria-label": "RoboSystems on X", className: linkClass, children: _jsx("svg", { className: "h-6 w-6", fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z" }) }) }), _jsx(Link, { href: "https://www.linkedin.com/company/robosystems", target: "_blank", rel: "noopener noreferrer", "aria-label": "RoboSystems on LinkedIn", className: linkClass, children: _jsx("svg", { className: "h-6 w-6", fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" }) }) }), _jsx(Link, { href: "https://www.youtube.com/@RoboSystems", target: "_blank", rel: "noopener noreferrer", "aria-label": "RoboSystems on YouTube", className: linkClass, children: _jsx("svg", { className: "h-6 w-6", fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" }) }) })] })] }), _jsxs("div", { children: [_jsx("h3", { className: headingClass, children: "Product" }), _jsx("ul", { className: "space-y-2 text-sm", children: productLinks.map((link) => (_jsx("li", { children: _jsx(Link, { href: link.href, className: linkClass, children: link.label }) }, link.href))) })] }), _jsxs("div", { children: [_jsx("h3", { className: headingClass, children: "Applications" }), _jsx("ul", { className: "space-y-2 text-sm", children: APP_ORDER.map((app) => {
|
|
28
28
|
const cfg = getAppConfig(app);
|
|
29
29
|
const isCurrent = app === CURRENT_APP;
|
|
30
30
|
return (_jsx("li", { children: _jsx(Link, Object.assign({ href: cfg.url }, (isCurrent
|
|
31
31
|
? {}
|
|
32
32
|
: { target: '_blank', rel: 'noopener noreferrer' }), { className: linkClass, children: cfg.displayName })) }, app));
|
|
33
|
-
}) })] }), _jsxs("div", { children: [_jsx("h3", { className: headingClass, children: "Company" }), _jsxs("ul", { className: "space-y-2 text-sm", children: [_jsx("li", { children: _jsx(Link, Object.assign({ href: `${companyBase}/research` }, companyLinkProps, { className: linkClass, children: "Research" })) }), _jsx("li", { children: _jsx(Link, { href: "https://holon.robosystems.ai", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "Holon Viewer" }) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: `${companyBase}/blog` }, companyLinkProps, { className: linkClass, children: "Blog" })) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: `${companyBase}/pages/privacy` }, companyLinkProps, { className: linkClass, children: "Privacy" })) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: `${companyBase}/pages/terms` }, companyLinkProps, { className: linkClass, children: "Terms" })) }), _jsx("li", { children: _jsx(Link, { href: "https://trust.robosystems.ai", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "Trust Center" }) }), ContactModal && (_jsx("li", { children: _jsx("button", { onClick: () => setShowContact(true), className: linkClass, children: "Contact" }) }))] })] })] }), _jsx("div", { className: "mt-8 border-t border-gray-800 pt-8 text-center text-sm text-gray-400", children: _jsxs("p", { children: ["\u00A9 ", new Date().getFullYear(), " RFS LLC. All rights reserved."] }) })] }), ContactModal && (_jsx(ContactModal, { isOpen: showContact, onClose: () => setShowContact(false) }))] }));
|
|
33
|
+
}) })] }), _jsxs("div", { children: [_jsx("h3", { className: headingClass, children: "Company" }), _jsxs("ul", { className: "space-y-2 text-sm", children: [_jsx("li", { children: _jsx(Link, Object.assign({ href: `${companyBase}/research` }, companyLinkProps, { className: linkClass, children: "Research" })) }), _jsx("li", { children: _jsx(Link, { href: "https://holon.robosystems.ai", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "Holon Viewer" }) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: `${companyBase}/blog` }, companyLinkProps, { className: linkClass, children: "Blog" })) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: `${companyBase}/pages/privacy` }, companyLinkProps, { className: linkClass, children: "Privacy" })) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: `${companyBase}/pages/terms` }, companyLinkProps, { className: linkClass, children: "Terms" })) }), _jsx("li", { children: _jsx(Link, { href: "https://trust.robosystems.ai", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "Trust Center" }) }), _jsx("li", { children: _jsx(Link, { href: "https://status.robosystems.ai", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "Status" }) }), ContactModal && (_jsx("li", { children: _jsx("button", { onClick: () => setShowContact(true), className: linkClass, children: "Contact" }) }))] })] })] }), _jsx("div", { className: "mt-8 border-t border-gray-800 pt-8 text-center text-sm text-gray-400", children: _jsxs("p", { children: ["\u00A9 ", new Date().getFullYear(), " RFS LLC. All rights reserved."] }) })] }), ContactModal && (_jsx(ContactModal, { isOpen: showContact, onClose: () => setShowContact(false) }))] }));
|
|
34
34
|
}
|