parent
e849eeeca5
commit
33d7257ed2
15 changed files with 199 additions and 374 deletions
@ -1,50 +0,0 @@ |
||||
--- |
||||
import Popup from './Popup/Popup.astro'; |
||||
import CaptchaFields from './Captcha/CaptchaFields.astro'; |
||||
--- |
||||
|
||||
<Popup id='download-popup' title='Download' subtitle='Enter your email below to receive the download link.'> |
||||
<form |
||||
action='https://news.roadmap.sh/subscribe' |
||||
method='POST' |
||||
accept-charset='utf-8' |
||||
target='_blank' |
||||
captcha-form |
||||
> |
||||
<input type='hidden' name='gdpr' value='true' /> |
||||
|
||||
<input |
||||
type='email' |
||||
name='email' |
||||
id='email' |
||||
required |
||||
autofocus |
||||
class='w-full rounded-md border text-md py-2.5 px-3 mb-2' |
||||
placeholder='Enter your Email' |
||||
/> |
||||
|
||||
<CaptchaFields /> |
||||
|
||||
<input type='hidden' name='list' value='tTqz1w7nexY3cWDpLnI88Q' /> |
||||
<input type='hidden' name='subform' value='yes' /> |
||||
|
||||
<button |
||||
type='submit' |
||||
name='submit' |
||||
class='text-white bg-gradient-to-r from-amber-700 to-blue-800 hover:from-amber-800 hover:to-blue-900 font-regular rounded-md text-md px-5 py-2.5 w-full text-center mr-2' |
||||
submit-download-form |
||||
> |
||||
Send Link |
||||
</button> |
||||
</form> |
||||
</Popup> |
||||
|
||||
<script> |
||||
document.querySelector('[submit-download-form]')?.addEventListener('click', () => { |
||||
window.fireEvent({ |
||||
category: 'Subscription', |
||||
action: 'Submitted Popup Form', |
||||
label: 'Download Roadmap Popup', |
||||
}); |
||||
}); |
||||
</script> |
@ -1,45 +0,0 @@ |
||||
import { useAuth } from '../../hooks/use-auth'; |
||||
|
||||
export default function ProfileDetails() { |
||||
const { user, isLoading } = useAuth(); |
||||
return ( |
||||
<div className="py-10 pb-20"> |
||||
<h1 className="text-3xl font-bold sm:text-4xl">Profile</h1> |
||||
<p className="mt-2">Here you can view your profile details.</p> |
||||
<div className="mt-5 space-y-4"> |
||||
<div> |
||||
<label className="text-slate-500">Name</label> |
||||
<div className="mt-1"> |
||||
{isLoading || !user ? ( |
||||
<Skeleton /> |
||||
) : ( |
||||
<h2 className="text-xl font-medium text-slate-800"> |
||||
{user?.name} |
||||
</h2> |
||||
)} |
||||
</div> |
||||
</div> |
||||
<div> |
||||
<label className="text-slate-500">Email</label> |
||||
<div className="mt-1"> |
||||
{isLoading || !user ? ( |
||||
<Skeleton className="w-64" /> |
||||
) : ( |
||||
<h2 className="text-xl font-medium text-slate-800"> |
||||
{user?.email} |
||||
</h2> |
||||
)} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
function Skeleton({ className }: { className?: string }) { |
||||
return ( |
||||
<div |
||||
className={`h-7 w-36 animate-pulse rounded-md bg-slate-100 ${className}`} |
||||
/> |
||||
); |
||||
} |
@ -1,219 +0,0 @@ |
||||
import { useCallback, useEffect, useState } from 'preact/hooks'; |
||||
import Cookies from 'js-cookie'; |
||||
import Spinner from '../Spinner'; |
||||
import {TOKEN_COOKIE_NAME} from "../../lib/jwt"; |
||||
|
||||
export default function ChangePasswordForm() { |
||||
const [authProvider, setAuthProvider] = useState< |
||||
'email' | 'google' | 'github' | null |
||||
>(null); |
||||
const [currentPassword, setCurrentPassword] = useState(''); |
||||
const [newPassword, setNewPassword] = useState(''); |
||||
const [newPasswordConfirmation, setNewPasswordConfirmation] = useState(''); |
||||
const [message, setMessage] = useState<{ |
||||
type: 'error' | 'success' | 'info'; |
||||
message: string; |
||||
}>(); |
||||
const [isLoading, setIsLoading] = useState(false); |
||||
|
||||
const handleSubmit = (e: Event) => { |
||||
e.preventDefault(); |
||||
setIsLoading(true); |
||||
|
||||
if (newPassword !== newPasswordConfirmation) { |
||||
setMessage({ |
||||
type: 'error', |
||||
message: 'Passwords do not match', |
||||
}); |
||||
setIsLoading(false); |
||||
return; |
||||
} |
||||
|
||||
const headers = new Headers(); |
||||
headers.append('Content-Type', 'application/json'); |
||||
headers.append( |
||||
'Cookie', |
||||
`${TOKEN_COOKIE_NAME}=${Cookies.get(TOKEN_COOKIE_NAME)}` |
||||
); |
||||
|
||||
fetch('http://localhost:8080/v1-update-password', { |
||||
method: 'POST', |
||||
credentials: 'include', |
||||
headers, |
||||
body: JSON.stringify({ |
||||
oldPassword: authProvider === 'email' ? currentPassword : 'social-auth', |
||||
password: newPassword, |
||||
confirmPassword: newPasswordConfirmation, |
||||
}), |
||||
}) |
||||
.then(async (res) => { |
||||
const json = await res.json(); |
||||
if (res.ok) { |
||||
return json; |
||||
} else { |
||||
throw new Error(json.message); |
||||
} |
||||
}) |
||||
.then((data) => { |
||||
setIsLoading(false); |
||||
setCurrentPassword(''); |
||||
setNewPassword(''); |
||||
setNewPasswordConfirmation(''); |
||||
fetchProfile(); |
||||
setMessage({ |
||||
type: 'success', |
||||
message: 'Password updated successfully', |
||||
}); |
||||
}) |
||||
.catch((err) => { |
||||
setIsLoading(false); |
||||
setMessage({ |
||||
type: 'error', |
||||
message: err.message, |
||||
}); |
||||
}); |
||||
}; |
||||
|
||||
const fetchProfile = useCallback(async () => { |
||||
// Set the loading state
|
||||
setIsLoading(true); |
||||
|
||||
// Create headers with the cookie
|
||||
const headers = new Headers(); |
||||
headers.append('Content-Type', 'application/json'); |
||||
headers.append( |
||||
'Cookie', |
||||
`${TOKEN_COOKIE_NAME}=${Cookies.get(TOKEN_COOKIE_NAME)}` |
||||
); |
||||
|
||||
try { |
||||
const res = await fetch('http://localhost:8080/v1-me', { |
||||
method: 'POST', |
||||
credentials: 'include', |
||||
headers, |
||||
}); |
||||
|
||||
const json = await res.json(); |
||||
|
||||
if (json.status === 401) { |
||||
// If the user is not authenticated, redirect to the login page
|
||||
// Clear the cookie
|
||||
Cookies.remove(TOKEN_COOKIE_NAME); |
||||
window.location.href = '/login'; |
||||
} |
||||
|
||||
if (res.ok) { |
||||
setAuthProvider(json.authProvider); |
||||
} else { |
||||
throw new Error(json.message); |
||||
} |
||||
} catch (error: any) { |
||||
setMessage({ |
||||
type: 'error', |
||||
message: error?.message || 'Something went wrong', |
||||
}); |
||||
} |
||||
setIsLoading(false); |
||||
}, []); |
||||
|
||||
// Make a request to the backend to fill in the form with the current values
|
||||
useEffect(() => { |
||||
fetchProfile(); |
||||
}, []); |
||||
|
||||
return ( |
||||
<form onSubmit={handleSubmit}> |
||||
<h2 className="text-3xl font-bold sm:text-4xl">Password</h2> |
||||
<p className="mt-2">Manage settings for your account passwords</p> |
||||
<div className="mt-8 space-y-4"> |
||||
{authProvider === 'email' && ( |
||||
<div className="flex w-full flex-col"> |
||||
<label |
||||
for="current-password" |
||||
className="text-sm leading-none text-slate-500" |
||||
> |
||||
Current Password |
||||
</label> |
||||
<input |
||||
type="password" |
||||
name="current-password" |
||||
id="current-password" |
||||
className="mt-2 block w-full appearance-none rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none transition duration-150 ease-in-out placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" |
||||
required |
||||
minLength={6} |
||||
placeholder="Current password" |
||||
value={currentPassword} |
||||
onInput={(e) => |
||||
setCurrentPassword((e.target as HTMLInputElement).value) |
||||
} |
||||
/> |
||||
</div> |
||||
)} |
||||
<div className="flex w-full flex-col"> |
||||
<label |
||||
for="new-password" |
||||
className="text-sm leading-none text-slate-500" |
||||
> |
||||
New Password |
||||
</label> |
||||
<input |
||||
type="password" |
||||
name="new-password" |
||||
id="new-password" |
||||
className="mt-2 block w-full appearance-none rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none transition duration-150 ease-in-out placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" |
||||
required |
||||
minLength={6} |
||||
placeholder="New password" |
||||
value={newPassword} |
||||
onInput={(e) => |
||||
setNewPassword((e.target as HTMLInputElement).value) |
||||
} |
||||
/> |
||||
</div> |
||||
<div className="flex w-full flex-col"> |
||||
<label |
||||
for="new-password-confirmation" |
||||
className="text-sm leading-none text-slate-500" |
||||
> |
||||
New Password Confirm |
||||
</label> |
||||
<input |
||||
type="password" |
||||
name="new-password-confirmation" |
||||
id="new-password-confirmation" |
||||
className="mt-2 block w-full appearance-none rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none transition duration-150 ease-in-out placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" |
||||
required |
||||
minLength={6} |
||||
placeholder="New password confirm" |
||||
value={newPasswordConfirmation} |
||||
onInput={(e) => |
||||
setNewPasswordConfirmation((e.target as HTMLInputElement).value) |
||||
} |
||||
/> |
||||
</div> |
||||
|
||||
{message && ( |
||||
<div |
||||
className={`mt-2 rounded-lg p-2 ${ |
||||
message.type === 'error' |
||||
? 'bg-red-100 text-red-700' |
||||
: message.type === 'success' |
||||
? 'bg-green-100 text-green-700' |
||||
: 'bg-blue-100 text-blue-700' |
||||
}`}
|
||||
> |
||||
{message.message} |
||||
</div> |
||||
)} |
||||
|
||||
<button |
||||
className="!mt-5 inline-flex h-10 min-w-[120px] items-center justify-center rounded-lg border border-slate-300 bg-black p-2 px-4 text-sm font-medium text-white outline-none transition duration-150 ease-in-out focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60" |
||||
type="submit" |
||||
disabled={isLoading} |
||||
> |
||||
{isLoading ? <Spinner className="text-white" /> : 'Change'} |
||||
</button> |
||||
</div> |
||||
</form> |
||||
); |
||||
} |
@ -0,0 +1,181 @@ |
||||
import { useCallback, useEffect, useState } from 'preact/hooks'; |
||||
import Cookies from 'js-cookie'; |
||||
import Spinner from '../Spinner'; |
||||
import { TOKEN_COOKIE_NAME } from '../../lib/jwt'; |
||||
import { httpGet, httpPost } from '../../lib/http'; |
||||
|
||||
export default function UpdatePasswordForm() { |
||||
const [authProvider, setAuthProvider] = useState(''); |
||||
const [currentPassword, setCurrentPassword] = useState(''); |
||||
const [newPassword, setNewPassword] = useState(''); |
||||
const [newPasswordConfirmation, setNewPasswordConfirmation] = useState(''); |
||||
|
||||
const [error, setError] = useState(''); |
||||
const [success, setSuccess] = useState(''); |
||||
|
||||
const [isLoading, setIsLoading] = useState(false); |
||||
|
||||
const handleSubmit = async (e: Event) => { |
||||
e.preventDefault(); |
||||
setIsLoading(true); |
||||
setError(''); |
||||
setSuccess(''); |
||||
|
||||
if (newPassword !== newPasswordConfirmation) { |
||||
setError('Passwords do not match'); |
||||
setIsLoading(false); |
||||
|
||||
return; |
||||
} |
||||
|
||||
const { response, error } = await httpPost( |
||||
`${import.meta.env.PUBLIC_API_URL}/v1-update-password`, |
||||
{ |
||||
oldPassword: authProvider === 'email' ? currentPassword : 'social-auth', |
||||
password: newPassword, |
||||
confirmPassword: newPasswordConfirmation, |
||||
} |
||||
); |
||||
|
||||
if (error) { |
||||
setError(error.message || 'Something went wrong'); |
||||
setIsLoading(false); |
||||
|
||||
return; |
||||
} |
||||
|
||||
setError(''); |
||||
setCurrentPassword(''); |
||||
setNewPassword(''); |
||||
setNewPasswordConfirmation(''); |
||||
setSuccess('Password updated successfully'); |
||||
setIsLoading(false); |
||||
}; |
||||
|
||||
const loadProfile = async () => { |
||||
setIsLoading(true); |
||||
|
||||
const { error, response } = await httpGet( |
||||
`${import.meta.env.PUBLIC_API_URL}/v1-me` |
||||
); |
||||
|
||||
if (error || !response) { |
||||
if (error?.status === 401) { |
||||
Cookies.remove(TOKEN_COOKIE_NAME); |
||||
window.location.reload(); |
||||
|
||||
return; |
||||
} |
||||
|
||||
setIsLoading(false); |
||||
setError(error?.message || 'Something went wrong'); |
||||
|
||||
return; |
||||
} |
||||
|
||||
const { authProvider } = response; |
||||
setAuthProvider(authProvider); |
||||
|
||||
setIsLoading(false); |
||||
}; |
||||
|
||||
useEffect(() => { |
||||
loadProfile().finally(() => { |
||||
// Hide page loader
|
||||
}); |
||||
}, []); |
||||
|
||||
return ( |
||||
<form onSubmit={handleSubmit}> |
||||
<h2 className="text-3xl font-bold sm:text-4xl">Password</h2> |
||||
<p className="mt-2">Manage settings for your account passwords</p> |
||||
<div className="mt-8 space-y-4"> |
||||
{authProvider === 'email' && ( |
||||
<div className="flex w-full flex-col"> |
||||
<label |
||||
for="current-password" |
||||
className="text-sm leading-none text-slate-500" |
||||
> |
||||
Current Password |
||||
</label> |
||||
<input |
||||
disabled={authProvider !== 'email'} |
||||
type="password" |
||||
name="current-password" |
||||
id="current-password" |
||||
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-100" |
||||
required |
||||
minLength={6} |
||||
placeholder="Current password" |
||||
value={currentPassword} |
||||
onInput={(e) => |
||||
setCurrentPassword((e.target as HTMLInputElement).value) |
||||
} |
||||
/> |
||||
</div> |
||||
)} |
||||
|
||||
<div className="flex w-full flex-col"> |
||||
<label |
||||
for="new-password" |
||||
className="text-sm leading-none text-slate-500" |
||||
> |
||||
New Password |
||||
</label> |
||||
<input |
||||
type="password" |
||||
name="new-password" |
||||
id="new-password" |
||||
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" |
||||
required |
||||
minLength={6} |
||||
placeholder="New password" |
||||
value={newPassword} |
||||
onInput={(e) => |
||||
setNewPassword((e.target as HTMLInputElement).value) |
||||
} |
||||
/> |
||||
</div> |
||||
<div className="flex w-full flex-col"> |
||||
<label |
||||
for="new-password-confirmation" |
||||
className="text-sm leading-none text-slate-500" |
||||
> |
||||
New Password Confirm |
||||
</label> |
||||
<input |
||||
type="password" |
||||
name="new-password-confirmation" |
||||
id="new-password-confirmation" |
||||
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" |
||||
required |
||||
minLength={6} |
||||
placeholder="New password confirm" |
||||
value={newPasswordConfirmation} |
||||
onInput={(e) => |
||||
setNewPasswordConfirmation((e.target as HTMLInputElement).value) |
||||
} |
||||
/> |
||||
</div> |
||||
|
||||
{error && ( |
||||
<p class="mt-2 rounded-lg bg-red-100 p-2 text-red-700">{error}</p> |
||||
)} |
||||
|
||||
{success && ( |
||||
<p class="mt-2 rounded-lg bg-green-100 p-2 text-green-700"> |
||||
{success} |
||||
</p> |
||||
)} |
||||
|
||||
<button |
||||
type="submit" |
||||
disabled={isLoading} |
||||
className="inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-none focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400" |
||||
> |
||||
{isLoading ? 'Please wait...' : 'Update Password'} |
||||
</button> |
||||
</div> |
||||
</form> |
||||
); |
||||
} |
@ -1,41 +0,0 @@ |
||||
--- |
||||
import Popup from './Popup/Popup.astro'; |
||||
import CaptchaFields from './Captcha/CaptchaFields.astro'; |
||||
--- |
||||
|
||||
<Popup id='subscribe-popup' title='Subscribe' subtitle='Enter your email below to receive updates.'> |
||||
<form |
||||
action='https://news.roadmap.sh/subscribe' |
||||
method='POST' |
||||
accept-charset='utf-8' |
||||
target='_blank' |
||||
captcha-form |
||||
> |
||||
<input type='hidden' name='gdpr' value='true' /> |
||||
|
||||
<input |
||||
type='email' |
||||
name='email' |
||||
required |
||||
autofocus |
||||
class='w-full rounded-md border text-md py-2.5 px-3 mb-2' |
||||
placeholder='Enter your Email' |
||||
/> |
||||
|
||||
<CaptchaFields /> |
||||
|
||||
<input type='hidden' name='list' value='tTqz1w7nexY3cWDpLnI88Q' /> |
||||
<input type='hidden' name='subform' value='yes' /> |
||||
|
||||
<button |
||||
type='submit' |
||||
name='submit' |
||||
class='text-white bg-gradient-to-r from-amber-700 to-blue-800 hover:from-amber-800 hover:to-blue-900 font-regular rounded-md text-md px-5 py-2.5 w-full text-center mr-2' |
||||
ga-category='Subscription' |
||||
ga-action='Submitted Popup Form' |
||||
ga-label='Subscribe Roadmap Popup' |
||||
> |
||||
Subscribe |
||||
</button> |
||||
</form> |
||||
</Popup> |
@ -1,11 +1,11 @@ |
||||
--- |
||||
import ChangePasswordForm from '../../components/Setting/ChangePasswordForm'; |
||||
import UpdatePasswordForm from '../../components/Setting/UpdatePasswordForm'; |
||||
import SettingSidebar from '../../components/Setting/SettingSidebar.astro'; |
||||
import SettingLayout from '../../layouts/SettingLayout.astro'; |
||||
--- |
||||
|
||||
<SettingLayout title='Change Password' description=''> |
||||
<SettingSidebar pageUrl='change-password' name='Change Password'> |
||||
<ChangePasswordForm client:load /> |
||||
<UpdatePasswordForm client:load /> |
||||
</SettingSidebar> |
||||
</SettingLayout> |
Loading…
Reference in new issue