phanpy/src/pages/login.jsx

221 lines
6.3 KiB
JavaScript

import './login.css';
import Fuse from 'fuse.js';
import { useEffect, useRef, useState } from 'preact/hooks';
import { useSearchParams } from 'react-router-dom';
import logo from '../assets/logo.svg';
import Link from '../components/link';
import Loader from '../components/loader';
import instancesListURL from '../data/instances.json?url';
import { getAuthorizationURL, registerApplication } from '../utils/auth';
import store from '../utils/store';
import useTitle from '../utils/useTitle';
const { PHANPY_DEFAULT_INSTANCE: DEFAULT_INSTANCE } = import.meta.env;
function Login() {
useTitle('Log in');
const instanceURLRef = useRef();
const cachedInstanceURL = store.local.get('instanceURL');
const [uiState, setUIState] = useState('default');
const [searchParams] = useSearchParams();
const instance = searchParams.get('instance');
const submit = searchParams.get('submit');
const [instanceText, setInstanceText] = useState(
instance || cachedInstanceURL?.toLowerCase() || '',
);
const [instancesList, setInstancesList] = useState([]);
const searcher = useRef();
useEffect(() => {
(async () => {
try {
const res = await fetch(instancesListURL);
const data = await res.json();
setInstancesList(data);
searcher.current = new Fuse(data);
} catch (e) {
// Silently fail
console.error(e);
}
})();
}, []);
// useEffect(() => {
// if (cachedInstanceURL) {
// instanceURLRef.current.value = cachedInstanceURL.toLowerCase();
// }
// }, []);
const submitInstance = (instanceURL) => {
if (!instanceURL) return;
store.local.set('instanceURL', instanceURL);
(async () => {
setUIState('loading');
try {
const { client_id, client_secret, vapid_key } =
await registerApplication({
instanceURL,
});
if (client_id && client_secret) {
store.session.set('clientID', client_id);
store.session.set('clientSecret', client_secret);
store.session.set('vapidKey', vapid_key);
location.href = await getAuthorizationURL({
instanceURL,
client_id,
});
} else {
alert('Failed to register application');
}
setUIState('default');
} catch (e) {
console.error(e);
setUIState('error');
}
})();
};
const cleanInstanceText = instanceText
? instanceText
.replace(/^https?:\/\//, '') // Remove protocol from instance URL
.replace(/\/+$/, '') // Remove trailing slash
.replace(/^@?[^@]+@/, '') // Remove @?acct@
.trim()
: null;
const instanceTextLooksLikeDomain =
/[^\s\r\n\t\/\\]+\.[^\s\r\n\t\/\\]+/.test(cleanInstanceText) &&
!/[\s\/\\@]/.test(cleanInstanceText);
const instancesSuggestions = cleanInstanceText
? searcher.current
?.search(cleanInstanceText, {
limit: 10,
})
?.map((match) => match.item)
: [];
const selectedInstanceText = instanceTextLooksLikeDomain
? cleanInstanceText
: instancesSuggestions?.length
? instancesSuggestions[0]
: instanceText
? instancesList.find((instance) => instance.includes(instanceText))
: null;
const onSubmit = (e) => {
e.preventDefault();
// const { elements } = e.target;
// let instanceURL = elements.instanceURL.value.toLowerCase();
// // Remove protocol from instance URL
// instanceURL = instanceURL.replace(/^https?:\/\//, '').replace(/\/+$/, '');
// // Remove @acct@ or acct@ from instance URL
// instanceURL = instanceURL.replace(/^@?[^@]+@/, '');
// if (!/\./.test(instanceURL)) {
// instanceURL = instancesList.find((instance) =>
// instance.includes(instanceURL),
// );
// }
// submitInstance(instanceURL);
submitInstance(selectedInstanceText);
};
if (submit) {
useEffect(() => {
submitInstance(instance || selectedInstanceText);
}, []);
}
return (
<main id="login" style={{ textAlign: 'center' }}>
<form onSubmit={onSubmit}>
<h1>
<img src={logo} alt="" width="80" height="80" />
<br />
Log in
</h1>
<label>
<p>Instance</p>
<input
value={instanceText}
required
type="text"
class="large"
id="instanceURL"
ref={instanceURLRef}
disabled={uiState === 'loading'}
// list="instances-list"
autocorrect="off"
autocapitalize="off"
autocomplete="off"
spellCheck={false}
placeholder="instance domain"
onInput={(e) => {
setInstanceText(e.target.value);
}}
/>
{instancesSuggestions?.length > 0 ? (
<ul id="instances-suggestions">
{instancesSuggestions.map((instance, i) => (
<li>
<button
type="button"
class="plain5"
onClick={() => {
submitInstance(instance);
}}
>
{instance}
</button>
</li>
))}
</ul>
) : (
<div id="instances-eg">e.g. &ldquo;mastodon.social&rdquo;</div>
)}
{/* <datalist id="instances-list">
{instancesList.map((instance) => (
<option value={instance} />
))}
</datalist> */}
</label>
{uiState === 'error' && (
<p class="error">
Failed to log in. Please try again or another instance.
</p>
)}
<div>
<button
disabled={
uiState === 'loading' || !instanceText || !selectedInstanceText
}
>
{selectedInstanceText
? `Continue with ${selectedInstanceText}`
: 'Continue'}
</button>{' '}
</div>
<Loader hidden={uiState !== 'loading'} />
<hr />
{!DEFAULT_INSTANCE && (
<p>
<a href="https://joinmastodon.org/servers" target="_blank">
Don't have an account? Create one!
</a>
</p>
)}
<p>
<Link to="/">Go home</Link>
</p>
</form>
</main>
);
}
export default Login;