change(ui) - login changes

This commit is contained in:
Shekar Siri 2023-03-24 15:58:07 +01:00
parent 561f333870
commit 7adf87a323
9 changed files with 294 additions and 240 deletions

View file

@ -14,21 +14,23 @@ const LOGIN = loginRoute();
const recaptchaRef = React.createRef();
const ERROR_DONT_MATCH = "Passwords don't match.";
const MIN_LENGTH = 8;
const PASSWORD_POLICY = `Password should contain at least ${ MIN_LENGTH } symbols.`;
const PASSWORD_POLICY = `Password should contain at least ${MIN_LENGTH} symbols.`;
const checkDontMatch = (newPassword, newPasswordRepeat) =>
newPasswordRepeat.length > 0 && newPasswordRepeat !== newPassword;
@connect(
(state, props) => ({
errors: state.getIn([ 'user', 'requestResetPassowrd', 'errors' ]),
resetErrors: state.getIn([ 'user', 'resetPassword', 'errors' ]),
loading: state.getIn([ 'user', 'requestResetPassowrd', 'loading' ]) || state.getIn([ 'user', 'resetPassword', 'loading' ]),
params: new URLSearchParams(props.location.search)
errors: state.getIn(['user', 'requestResetPassowrd', 'errors']),
resetErrors: state.getIn(['user', 'resetPassword', 'errors']),
loading:
state.getIn(['user', 'requestResetPassowrd', 'loading']) ||
state.getIn(['user', 'resetPassword', 'loading']),
params: new URLSearchParams(props.location.search),
}),
{ requestResetPassword, resetPassword, resetErrors },
{ requestResetPassword, resetPassword, resetErrors }
)
@withPageTitle("Password Reset - OpenReplay")
@withPageTitle('Password Reset - OpenReplay')
@withRouter
export default class ForgotPassword extends React.PureComponent {
state = {
@ -45,15 +47,17 @@ export default class ForgotPassword extends React.PureComponent {
const { email, password } = this.state;
const { params } = this.props;
const pass = params.get('pass')
const invitation = params.get('invitation')
const resetting = pass && invitation
const pass = params.get('pass');
const invitation = params.get('invitation');
const resetting = pass && invitation;
if (!resetting) {
this.props.requestResetPassword({ email: email.trim(), 'g-recaptcha-response': token }).then(() => {
const { errors } = this.props;
if (!errors) this.setState({ requested: true });
});
this.props
.requestResetPassword({ email: email.trim(), 'g-recaptcha-response': token })
.then(() => {
const { errors } = this.props;
if (!errors) this.setState({ requested: true });
});
} else {
if (this.isSubmitDisabled()) return;
this.props.resetPassword({ email: email.trim(), invitation, pass, password }).then(() => {
@ -61,16 +65,15 @@ export default class ForgotPassword extends React.PureComponent {
if (!resetErrors) this.setState({ updated: true });
});
}
}
};
isSubmitDisabled() {
const { password, passwordRepeat } = this.state;
if (password !== passwordRepeat ||
password.length < MIN_LENGTH) return true;
if (password !== passwordRepeat || password.length < MIN_LENGTH) return true;
return false;
}
write = ({ target: { value, name } }) => this.setState({ [ name ]: value })
write = ({ target: { value, name } }) => this.setState({ [name]: value });
shouldShouwPolicy() {
const { password } = this.state;
@ -79,152 +82,170 @@ export default class ForgotPassword extends React.PureComponent {
return true;
}
onSubmit = e => {
onSubmit = (e) => {
e.preventDefault();
const { CAPTCHA_ENABLED } = this.state;
if (CAPTCHA_ENABLED && recaptchaRef.current) {
recaptchaRef.current.execute()
recaptchaRef.current.execute();
} else if (!CAPTCHA_ENABLED) {
this.handleSubmit();
}
}
};
componentWillUnmount() {
this.props.resetErrors()
this.props.resetErrors();
}
render() {
const { CAPTCHA_ENABLED } = this.state;
const { errors, loading, params } = this.props;
const { requested, updated, password, passwordRepeat, email } = this.state;
const dontMatch = checkDontMatch(password, passwordRepeat);
const pass = params.get('pass')
const invitation = params.get('invitation')
const resetting = pass && invitation
const validEmail = validateEmail(email)
const dontMatch = checkDontMatch(password, passwordRepeat);
const pass = params.get('pass');
const invitation = params.get('invitation');
const resetting = pass && invitation;
const validEmail = validateEmail(email);
return (
<div className="flex" style={{ height: '100vh'}}>
<div className={cn("w-6/12", stl.left)}>
<div className="px-6 pt-10">
<img src="/assets/logo-white.svg" />
<div className="flex items-center justify-center h-screen">
<div className="flex flex-col items-center">
<div className="m-10 ">
<img src="/assets/logo.svg" width={200} />
</div>
<div className="color-white text-lg flex items-center">
<div className="flex items-center justify-center w-full" style={{ height: 'calc(100vh - 130px)'}}>
<div className="text-4xl">Welcome Back!</div>
</div>
</div>
</div>
<div className="w-6/12 flex items-center justify-center">
<Form onSubmit={ this.onSubmit } style={{ minWidth: '50%' }} className="flex flex-col items-center justify-center">
<div className="mb-8">
<h2 className="text-center text-3xl mb-6">{`${resetting ? 'Create' : 'Reset'} Password`}</h2>
</div>
<Loader loading={ loading }>
<div data-hidden={ updated } className="w-full">
{ CAPTCHA_ENABLED && (
<div className={ stl.recaptcha }>
<ReCAPTCHA
ref={ recaptchaRef }
size="invisible"
data-hidden={ requested }
sitekey={ window.env.CAPTCHA_SITE_KEY }
onChange={ token => this.handleSubmit(token) }
/>
</div>
)}
<div className="border rounded bg-white" style={{ width: '350px' }}>
<h2 className="text-center text-2xl font-medium mb-6 border-b p-5 w-full">
{ resetting ? "Create Password" : "Reset Password" }
</h2>
{ !resetting && !requested &&
<Form.Field>
<i className={ stl.inputIconUser } />
<Input
autoFocus={true}
autocomplete="email"
type="text"
placeholder="Email"
name="email"
onChange={ this.write }
className="w-full"
icon="user-alt"
/>
</Form.Field>
}
{
requested && !errors && (
<div>Reset password link has been sent to your email.</div>
)
}
{
resetting && (
<React.Fragment>
<Form.Field>
<i className={ stl.inputIconPassword } />
<Input
autocomplete="new-password"
type="password"
placeholder="Password"
name="password"
onChange={ this.write }
className="w-full"
<div className="px-8">
{resetting && <div className="my-6">Provide your email address, so we can send you a link to reset your password.</div> }
<Form
onSubmit={this.onSubmit}
style={{ minWidth: '50%' }}
className="flex flex-col items-center justify-center"
>
{/* <div className="mb-8">
<h2 className="text-center text-3xl mb-6">{`${
resetting ? 'Create' : 'Reset'
} Password`}</h2>
</div> */}
<Loader loading={loading}>
<div data-hidden={updated} className="w-full">
{CAPTCHA_ENABLED && (
<div className={stl.recaptcha}>
<ReCAPTCHA
ref={recaptchaRef}
size="invisible"
data-hidden={requested}
sitekey={window.env.CAPTCHA_SITE_KEY}
onChange={(token) => this.handleSubmit(token)}
/>
</Form.Field>
<div className={ stl.passwordPolicy } data-hidden={ !this.shouldShouwPolicy() }>
{ PASSWORD_POLICY }
</div>
)}
{!resetting && !requested && (
<Form.Field>
<i className={ stl.inputIconPassword } />
<label>{'Email Address:'}</label>
<Input
autocomplete="new-password"
type="password"
placeholder="Confirm Password"
name="passwordRepeat"
onChange={ this.write }
autoFocus={true}
autocomplete="email"
type="text"
placeholder="Email"
name="email"
onChange={this.write}
className="w-full"
icon="envelope"
/>
</Form.Field>
</React.Fragment>
)
}
)}
<Message error hidden={ !dontMatch }>
{ ERROR_DONT_MATCH }
</Message>
</div>
</Loader>
<div className="mt-4">
{ errors &&
<div className={ stl.errors }>
{ errors.map(error => <span>{ error }<br /></span>) }
{requested && !errors && (
<div className="flex flex-col items-center justify-center">
<div className="w-16 h-16 rounded-full bg-tealx-light flex items-center justify-center mb-2">
<Icon name="envelope-check" size={30} color="tealx" />
</div>
<div>Alright! a reset link was emailed to {email}. Click on it to reset your account password.</div>
</div>
)}
{resetting && (
<React.Fragment>
<Form.Field>
<i className={stl.inputIconPassword} />
<Input
autocomplete="new-password"
type="password"
placeholder="Password"
name="password"
onChange={this.write}
className="w-full"
/>
</Form.Field>
<div className={stl.passwordPolicy} data-hidden={!this.shouldShouwPolicy()}>
{PASSWORD_POLICY}
</div>
<Form.Field>
<i className={stl.inputIconPassword} />
<Input
autocomplete="new-password"
type="password"
placeholder="Confirm Password"
name="passwordRepeat"
onChange={this.write}
className="w-full"
/>
</Form.Field>
</React.Fragment>
)}
<Message error hidden={!dontMatch}>
{ERROR_DONT_MATCH}
</Message>
</div>
</Loader>
<div className="mt-4">
{errors && (
<div className={stl.errors}>
{errors.map((error) => (
<span>
{error}
<br />
</span>
))}
</div>
)}
<div data-hidden={!updated} className={stl.success}>
<Icon name="check" size="30" color="green" />
{'Your password has been updated sucessfully.'}
</div>
</div>
}
<div data-hidden={ !updated } className={ stl.success }>
<Icon name="check" size="30" color="green" />
{ 'Your password has been updated sucessfully.' }
</div>
</div>
{/* <div className={ stl.formFooter }> */}
{!(updated || requested) && (
<Button
type="submit"
variant="primary"
loading={loading}
disabled={ (resetting && this.isSubmitDisabled()) || (!resetting && !validEmail)}
>
{ resetting ? 'Create' : 'Reset' }
</Button>
)}
<div className={ stl.links }>
<Link to={ LOGIN }>
{ updated && (<Button variant="primary" type="submit" primary >{ 'Login' }</Button>)}
<div data-hidden={ updated }>{'Back to Login'}</div>
</Link>
</div>
{/* </div> */}
</Form>
{!(updated || requested) && (
<Button
type="submit"
variant="primary"
loading={loading}
className="w-full"
disabled={(resetting && this.isSubmitDisabled()) || (!resetting && !validEmail)}
>
{resetting ? 'Create' : 'Email password reset link'}
</Button>
)}
<div className="my-8">
<Link to={LOGIN}>
{updated && (
<Button variant="primary" type="submit" primary>
{'Login'}
</Button>
)}
<div data-hidden={updated} className="link">{'Back to Login'}</div>
</Link>
</div>
</Form>
</div>
</div>
</div>
</div>
);

View file

@ -69,18 +69,16 @@ class Login extends React.Component {
const { CAPTCHA_ENABLED } = this.state;
return (
<div className="flex flex-col md:flex-row" style={{ height: '100vh' }}>
<div className={cn('md:w-6/12 relative overflow-hidden', stl.left)}>
<div className="px-6 pt-10">
<img src="/assets/logo-white.svg" />
<div className="flex items-center justify-center h-screen">
<div className="flex flex-col items-center">
<div className="m-10 ">
<img src="/assets/logo.svg" width={200}/>
</div>
<img style={{ width: '800px', position: 'absolute', bottom: -100, left: 0 }} src={LoginBg} />
</div>
<div className="md:w-6/12 flex items-center justify-center py-10">
<div className="">
<div className="border rounded bg-white">
<Form onSubmit={this.onSubmit} className="flex items-center justify-center flex-col">
<div className="mb-8">
<h2 className="text-center text-3xl mb-6">Login to OpenReplay</h2>
<h2 className="text-center text-2xl font-medium mb-6 border-b p-5 w-full">Login to your account</h2>
<div className="">
{!authDetails.tenants && (
<div className="text-center text-xl">
Don't have an account?{' '}
@ -99,19 +97,19 @@ class Login extends React.Component {
onChange={(token) => this.handleSubmit(token)}
/>
)}
<div style={{ width: '350px' }}>
<div style={{ width: '350px' }} className="px-8">
<div className="mb-6">
<label>Email</label>
<label>Email Address</label>
<Input
data-test-id={"login"}
autoFocus={true}
autoComplete="username"
type="text"
placeholder="Email"
placeholder="e.g. john@example.com"
name="email"
onChange={this.write}
required
icon="user-alt"
icon="envelope"
/>
</div>
<div className="mb-6">
@ -142,15 +140,16 @@ class Login extends React.Component {
))}
</div>
) : null}
{/* <div className={ stl.formFooter }> */}
<Button data-test-id={"log-button"} className="mt-2" type="submit" variant="primary">
{'Login'}
</Button>
<div className="px-8 w-full">
<Button data-test-id={"log-button"} className="mt-2 w-full text-center" type="submit" variant="primary">
{'Login'}
</Button>
<div className={cn(stl.links, 'text-lg')}>
<Link to={FORGOT_PASSWORD}>{'Forgot your password?'}</Link>
<div className="my-8">
<span className="color-gray-medium">Having trouble logging in?</span> <Link to={FORGOT_PASSWORD} className="link ml-1">{'Reset password'}</Link>
</div>
</div>
{/* </div> */}
</Form>
<div className={cn(stl.sso, 'py-2 flex flex-col items-center')}>
@ -182,6 +181,8 @@ class Login extends React.Component {
</Tooltip>
)}
</div>
</div>
</div>
</div>

View file

@ -19,24 +19,19 @@ const BulletItem = ({ text }) => (
export default class Signup extends React.Component {
render() {
return (
<div className="flex" style={{ height: '100vh' }}>
<div className={cn('w-6/12 relative overflow-hidden', stl.left)}>
<div className="px-6 pt-10">
<img src="/assets/logo-white.svg" />
</div>
<img
style={{ width: '800px', position: 'absolute', bottom: -100, left: 0 }}
src={RegisterBg}
/>
<div className="color-white text-lg flex items-center px-20 pt-32">
<div className="flex justify-center items-center gap-6" style={{ height: '100vh' }}>
<div className={cn('relative overflow-hidden')}>
<div className="text-lg flex items-center" style={{ width: '350px'}}>
<div>
<div className="flex items-center text-3xl font-bold mb-6">
OpenReplay Cloud{' '}
<div className="ml-2">
<Icon name="signup" size="28" color="white" />
<div className="flex items-end text-3xl font-bold mb-6">
<div className="">
<img src="/assets/logo.svg" width={200}/>
</div>{' '}
<div className="ml-2 text-lg color-gray-medium">
Cloud
</div>
</div>
<div>OpenReplay Cloud is the hosted version of our open-source project.</div>
<div className="border-b pb-2 mb-2">OpenReplay Cloud is the hosted version of our <a className="link" href="https://github.com/openreplay/openreplay" target="_blank">open-source</a> project.</div>
<div>Well manage hosting, scaling and upgrades.</div>
<div className="mt-8">
@ -47,7 +42,7 @@ export default class Signup extends React.Component {
</div>
</div>
</div>
<div className="w-6/12 flex items-center justify-center">
<div className="flex items-center justify-center">
<div className="">
<SignupForm />
</div>

View file

@ -1,26 +1,25 @@
import React from 'react'
import { Form, Input, Icon, Button, Link } from 'UI'
import { login } from 'App/routes'
import ReCAPTCHA from 'react-google-recaptcha'
import stl from './signup.module.css'
import React from 'react';
import { Form, Input, Icon, Button, Link } from 'UI';
import { login } from 'App/routes';
import ReCAPTCHA from 'react-google-recaptcha';
import stl from './signup.module.css';
import { signup } from 'Duck/user';
import { connect } from 'react-redux'
import Select from 'Shared/Select'
import { connect } from 'react-redux';
import Select from 'Shared/Select';
import { SITE_ID_STORAGE_KEY } from 'App/constants/storageKeys';
const LOGIN_ROUTE = login()
const recaptchaRef = React.createRef()
const LOGIN_ROUTE = login();
const recaptchaRef = React.createRef();
@connect(
state => ({
(state) => ({
tenants: state.getIn(['user', 'tenants']),
errors: state.getIn([ 'user', 'signupRequest', 'errors' ]),
loading: state.getIn([ 'user', 'signupRequest', 'loading' ]),
errors: state.getIn(['user', 'signupRequest', 'errors']),
loading: state.getIn(['user', 'signupRequest', 'loading']),
}),
{ signup },
{ signup }
)
export default class SignupForm extends React.Component {
state = {
tenantId: '',
fullname: '',
@ -36,21 +35,30 @@ export default class SignupForm extends React.Component {
if (props.errors && props.errors.size > 0 && state.reload) {
recaptchaRef.current.reset();
return {
reload: false
}
}
reload: false,
};
}
return null;
}
handleSubmit = (token) => {
const { tenantId, fullname, password, email, projectName, organizationName, auth } = this.state;
localStorage.removeItem(SITE_ID_STORAGE_KEY)
this.props.signup({ tenantId, fullname, password, email, projectName, organizationName, auth, 'g-recaptcha-response': token })
this.setState({ reload: true })
}
localStorage.removeItem(SITE_ID_STORAGE_KEY);
this.props.signup({
tenantId,
fullname,
password,
email,
projectName,
organizationName,
auth,
'g-recaptcha-response': token,
});
this.setState({ reload: true });
};
write = ({ target: { value, name } }) => this.setState({ [ name ]: value })
writeOption = ({ name, value }) => this.setState({ [ name ]: value.value });
write = ({ target: { value, name } }) => this.setState({ [name]: value });
writeOption = ({ name, value }) => this.setState({ [name]: value.value });
onSubmit = (e) => {
e.preventDefault();
@ -60,52 +68,51 @@ export default class SignupForm extends React.Component {
} else if (!CAPTCHA_ENABLED) {
this.handleSubmit();
}
}
};
render() {
const { loading, errors, tenants } = this.props;
const { CAPTCHA_ENABLED } = this.state;
return (
<Form onSubmit={ this.onSubmit }>
<Form onSubmit={this.onSubmit} className="bg-white border rounded">
<div className="mb-8">
<h2 className="text-center text-3xl mb-6">Get Started</h2>
<div className="text-center text-xl">Already having an account? <span className="link"><Link to={ LOGIN_ROUTE }>Sign in</Link></span></div>
<h2 className="text-center text-2xl font-medium mb-6 border-b p-5 w-full">Create Account</h2>
</div>
<>
{ CAPTCHA_ENABLED && (
{CAPTCHA_ENABLED && (
<ReCAPTCHA
ref={ recaptchaRef }
ref={recaptchaRef}
size="invisible"
sitekey={ window.env.CAPTCHA_SITE_KEY }
onChange={ token => this.handleSubmit(token) }
sitekey={window.env.CAPTCHA_SITE_KEY}
onChange={(token) => this.handleSubmit(token)}
/>
)}
<div>
{ tenants.length > 0 && (
<div className="px-8">
{tenants.length > 0 && (
<Form.Field>
<label>Existing Accounts</label>
<Select
className="w-full"
placeholder="Select account"
selection
options={ tenants }
options={tenants}
name="tenantId"
// value={ instance.currentPeriod }
onChange={ this.writeOption }
onChange={this.writeOption}
/>
</Form.Field>
)}
<Form.Field>
<label>Email</label>
<label>Email Address</label>
<Input
autoFocus={true}
autoComplete="username"
type="email"
placeholder="E.g. email@yourcompany.com"
name="email"
onChange={ this.write }
className={ stl.email }
onChange={this.write}
required="true"
icon="envelope"
/>
</Form.Field>
<Form.Field>
@ -115,57 +122,72 @@ export default class SignupForm extends React.Component {
placeholder="Min 8 Characters"
minLength="8"
name="password"
onChange={ this.write }
className={ stl.password }
onChange={this.write}
required="true"
icon="key"
/>
</Form.Field>
<Form.Field>
<label>Name</label>
<Input
<Input
type="text"
placeholder="E.g John Doe"
name="fullname"
onChange={ this.write }
className={ stl.email }
onChange={this.write}
required="true"
icon="user-alt"
/>
</Form.Field>
<Form.Field>
<label>Organization</label>
<Input
<Input
type="text"
placeholder="E.g Uber"
name="organizationName"
onChange={ this.write }
className={ stl.email }
onChange={this.write}
required="true"
icon="buildings"
/>
</Form.Field>
<div className="mb-6">
<div className="text-sm">By creating an account, you agree to our <a href="https://openreplay.com/terms.html" className="link">Terms of Service</a> and <a href="https://openreplay.com/privacy.html" className="link">Privacy Policy</a>.</div>
<Button type="submit" variant="primary" loading={loading} className="w-full">
Create account
</Button>
<div className="my-6">
<div className="text-sm">
By signing up, you agree to our{' '}
<a href="https://openreplay.com/terms.html" className="link">
terms of service
</a>{' '}
and{' '}
<a href="https://openreplay.com/privacy.html" className="link">
privacy policy
</a>
.
</div>
</div>
</div>
</>
{ errors &&
<div className={ stl.errors }>
{ errors.map(error => (
{errors && (
<div className={stl.errors}>
{errors.map((error) => (
<div className={stl.errorItem}>
<Icon name="info" color="red" size="20"/>
<span className="color-red ml-2">{ error }<br /></span>
<Icon name="info" color="red" size="20" />
<span className="color-red ml-2">
{error}
<br />
</span>
</div>
)) }
))}
</div>
)}
<div className="text-center bg-gray-50 py-4 border-t">
Already having an account?{' '}
<span className="link">
<Link to={LOGIN_ROUTE}>Login</Link>
</span>
</div>
}
<div className={ stl.formFooter }>
<Button type="submit" variant="primary" loading={loading}>
Create account
</Button>
</div>
</Form>
)
);
}
}
}

View file

@ -60,7 +60,7 @@ export default (props: Props) => {
}
const render = () => (
<button {...rest} type={type} className={cn(classes, className)}>
<button {...rest} type={type} className={cn(classes, className, 'flex items-center justify-center')}>
{icon && (
// @ts-ignore
<Icon className={cn({ 'mr-2': children })} name={icon} color={iconColor} size={iconSize} />

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-buildings" viewBox="0 0 16 16">
<path d="M14.763.075A.5.5 0 0 1 15 .5v15a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5V14h-1v1.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5V10a.5.5 0 0 1 .342-.474L6 7.64V4.5a.5.5 0 0 1 .276-.447l8-4a.5.5 0 0 1 .487.022ZM6 8.694 1 10.36V15h5V8.694ZM7 15h2v-1.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5V15h2V1.309l-7 3.5V15Z"/>
<path d="M2 11h1v1H2v-1Zm2 0h1v1H4v-1Zm-2 2h1v1H2v-1Zm2 0h1v1H4v-1Zm4-4h1v1H8V9Zm2 0h1v1h-1V9Zm-2 2h1v1H8v-1Zm2 0h1v1h-1v-1Zm2-2h1v1h-1V9Zm0 2h1v1h-1v-1ZM8 7h1v1H8V7Zm2 0h1v1h-1V7Zm2 0h1v1h-1V7ZM8 5h1v1H8V5Zm2 0h1v1h-1V5Zm2 0h1v1h-1V5Zm0-2h1v1h-1V3Z"/>
</svg>

After

Width:  |  Height:  |  Size: 672 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-envelope-check" viewBox="0 0 16 16">
<path d="M2 2a2 2 0 0 0-2 2v8.01A2 2 0 0 0 2 14h5.5a.5.5 0 0 0 0-1H2a1 1 0 0 1-.966-.741l5.64-3.471L8 9.583l7-4.2V8.5a.5.5 0 0 0 1 0V4a2 2 0 0 0-2-2H2Zm3.708 6.208L1 11.105V5.383l4.708 2.825ZM1 4.217V4a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v.217l-7 4.2-7-4.2Z"/>
<path d="M16 12.5a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0Zm-1.993-1.679a.5.5 0 0 0-.686.172l-1.17 1.95-.547-.547a.5.5 0 0 0-.708.708l.774.773a.75.75 0 0 0 1.174-.144l1.335-2.226a.5.5 0 0 0-.172-.686Z"/>
</svg>

After

Width:  |  Height:  |  Size: 574 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-key" viewBox="0 0 16 16">
<path d="M0 8a4 4 0 0 1 7.465-2H14a.5.5 0 0 1 .354.146l1.5 1.5a.5.5 0 0 1 0 .708l-1.5 1.5a.5.5 0 0 1-.708 0L13 9.207l-.646.647a.5.5 0 0 1-.708 0L11 9.207l-.646.647a.5.5 0 0 1-.708 0L9 9.207l-.646.647A.5.5 0 0 1 8 10h-.535A4 4 0 0 1 0 8zm4-3a3 3 0 1 0 2.712 4.285A.5.5 0 0 1 7.163 9h.63l.853-.854a.5.5 0 0 1 .708 0l.646.647.646-.647a.5.5 0 0 1 .708 0l.646.647.646-.647a.5.5 0 0 1 .708 0l.646.647.793-.793-1-1h-6.63a.5.5 0 0 1-.451-.285A3 3 0 0 0 4 5z"/>
<path d="M4 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 608 B