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 SettingSidebar from '../../components/Setting/SettingSidebar.astro'; |
||||||
import SettingLayout from '../../layouts/SettingLayout.astro'; |
import SettingLayout from '../../layouts/SettingLayout.astro'; |
||||||
--- |
--- |
||||||
|
|
||||||
<SettingLayout title='Change Password' description=''> |
<SettingLayout title='Change Password' description=''> |
||||||
<SettingSidebar pageUrl='change-password' name='Change Password'> |
<SettingSidebar pageUrl='change-password' name='Change Password'> |
||||||
<ChangePasswordForm client:load /> |
<UpdatePasswordForm client:load /> |
||||||
</SettingSidebar> |
</SettingSidebar> |
||||||
</SettingLayout> |
</SettingLayout> |
Loading…
Reference in new issue