mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-03-16 20:01:43 +08:00
Compare commits
10 Commits
fix/authen
...
fix/auth-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0652286bf | ||
|
|
92d562baed | ||
|
|
7ba48523da | ||
|
|
c1ebe9ae47 | ||
|
|
415a9c0fd0 | ||
|
|
d3b8cbceaa | ||
|
|
c390f4428e | ||
|
|
2e890b1b25 | ||
|
|
7b9be9377b | ||
|
|
4863f08a4c |
41
src/components/AuthenticationFlow/AuthenticationForm.tsx
Normal file
41
src/components/AuthenticationFlow/AuthenticationForm.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useState } from 'react';
|
||||
import { GitHubButton } from './GitHubButton';
|
||||
import { GoogleButton } from './GoogleButton';
|
||||
import { LinkedInButton } from './LinkedInButton';
|
||||
import { EmailLoginForm } from './EmailLoginForm';
|
||||
import { EmailSignupForm } from './EmailSignupForm';
|
||||
|
||||
type AuthenticationFormProps = {
|
||||
type?: 'login' | 'signup';
|
||||
};
|
||||
|
||||
export function AuthenticationForm(props: AuthenticationFormProps) {
|
||||
const { type = 'login' } = props;
|
||||
|
||||
const [isDisabled, setIsDisabled] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<GitHubButton isDisabled={isDisabled} setIsDisabled={setIsDisabled} />
|
||||
<GoogleButton isDisabled={isDisabled} setIsDisabled={setIsDisabled} />
|
||||
<LinkedInButton isDisabled={isDisabled} setIsDisabled={setIsDisabled} />
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-center gap-2 py-6 text-sm text-slate-600">
|
||||
<div className="h-px w-full bg-slate-200" />
|
||||
OR
|
||||
<div className="h-px w-full bg-slate-200" />
|
||||
</div>
|
||||
|
||||
{type === 'login' ? (
|
||||
<EmailLoginForm isDisabled={isDisabled} setIsDisabled={setIsDisabled} />
|
||||
) : (
|
||||
<EmailSignupForm
|
||||
isDisabled={isDisabled}
|
||||
setIsDisabled={setIsDisabled}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,9 +2,16 @@ import Cookies from 'js-cookie';
|
||||
import type { FormEvent } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { httpPost } from '../../lib/http';
|
||||
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
|
||||
import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt';
|
||||
|
||||
type EmailLoginFormProps = {
|
||||
isDisabled?: boolean;
|
||||
setIsDisabled?: (isDisabled: boolean) => void;
|
||||
};
|
||||
|
||||
export function EmailLoginForm(props: EmailLoginFormProps) {
|
||||
const { isDisabled, setIsDisabled } = props;
|
||||
|
||||
export function EmailLoginForm() {
|
||||
const [email, setEmail] = useState<string>('');
|
||||
const [password, setPassword] = useState<string>('');
|
||||
const [error, setError] = useState('');
|
||||
@@ -14,6 +21,7 @@ export function EmailLoginForm() {
|
||||
const handleFormSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setIsDisabled?.(true);
|
||||
setError('');
|
||||
|
||||
const { response, error } = await httpPost<{ token: string }>(
|
||||
@@ -26,11 +34,7 @@ export function EmailLoginForm() {
|
||||
|
||||
// Log the user in and reload the page
|
||||
if (response?.token) {
|
||||
Cookies.set(TOKEN_COOKIE_NAME, response.token, {
|
||||
path: '/',
|
||||
expires: 30,
|
||||
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
|
||||
});
|
||||
setAuthToken(response.token);
|
||||
window.location.reload();
|
||||
|
||||
return;
|
||||
@@ -45,6 +49,7 @@ export function EmailLoginForm() {
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
setIsDisabled?.(false);
|
||||
setError(error?.message || 'Something went wrong. Please try again later.');
|
||||
};
|
||||
|
||||
@@ -92,7 +97,7 @@ export function EmailLoginForm() {
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
disabled={isLoading || isDisabled}
|
||||
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...' : 'Continue'}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { type FormEvent, useState } from 'react';
|
||||
import { httpPost } from '../../lib/http';
|
||||
|
||||
export function EmailSignupForm() {
|
||||
type EmailSignupFormProps = {
|
||||
isDisabled?: boolean;
|
||||
setIsDisabled?: (isDisabled: boolean) => void;
|
||||
};
|
||||
|
||||
export function EmailSignupForm(props: EmailSignupFormProps) {
|
||||
const { isDisabled, setIsDisabled } = props;
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
@@ -13,6 +20,7 @@ export function EmailSignupForm() {
|
||||
e.preventDefault();
|
||||
|
||||
setIsLoading(true);
|
||||
setIsDisabled?.(true);
|
||||
setError('');
|
||||
|
||||
const { response, error } = await httpPost<{ status: 'ok' }>(
|
||||
@@ -21,20 +29,21 @@ export function EmailSignupForm() {
|
||||
email,
|
||||
password,
|
||||
name,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (error || response?.status !== 'ok') {
|
||||
setIsLoading(false);
|
||||
setIsDisabled?.(false);
|
||||
setError(
|
||||
error?.message || 'Something went wrong. Please try again later.'
|
||||
error?.message || 'Something went wrong. Please try again later.',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = `/verification-pending?email=${encodeURIComponent(
|
||||
email
|
||||
email,
|
||||
)}`;
|
||||
};
|
||||
|
||||
@@ -90,7 +99,7 @@ export function EmailSignupForm() {
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
disabled={isLoading || isDisabled}
|
||||
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...' : 'Continue to Verify Email'}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
|
||||
import Cookies from 'js-cookie';
|
||||
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
|
||||
import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
|
||||
type GitHubButtonProps = {};
|
||||
type GitHubButtonProps = {
|
||||
isDisabled?: boolean;
|
||||
setIsDisabled?: (isDisabled: boolean) => void;
|
||||
};
|
||||
|
||||
const GITHUB_REDIRECT_AT = 'githubRedirectAt';
|
||||
const GITHUB_LAST_PAGE = 'githubLastPage';
|
||||
|
||||
export function GitHubButton(props: GitHubButtonProps) {
|
||||
const { isDisabled, setIsDisabled } = props;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
@@ -25,6 +30,7 @@ export function GitHubButton(props: GitHubButtonProps) {
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setIsDisabled?.(true);
|
||||
httpGet<{ token: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-github-callback${
|
||||
window.location.search
|
||||
@@ -35,6 +41,7 @@ export function GitHubButton(props: GitHubButtonProps) {
|
||||
const errMessage = error?.message || 'Something went wrong.';
|
||||
setError(errMessage);
|
||||
setIsLoading(false);
|
||||
setIsDisabled?.(false);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -63,21 +70,19 @@ export function GitHubButton(props: GitHubButtonProps) {
|
||||
|
||||
localStorage.removeItem(GITHUB_REDIRECT_AT);
|
||||
localStorage.removeItem(GITHUB_LAST_PAGE);
|
||||
Cookies.set(TOKEN_COOKIE_NAME, response.token, {
|
||||
path: '/',
|
||||
expires: 30,
|
||||
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
|
||||
});
|
||||
setAuthToken(response.token);
|
||||
window.location.href = redirectUrl;
|
||||
})
|
||||
.catch((err) => {
|
||||
setError('Something went wrong. Please try again later.');
|
||||
setIsLoading(false);
|
||||
setIsDisabled?.(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleClick = async () => {
|
||||
setIsLoading(true);
|
||||
setIsDisabled?.(true);
|
||||
|
||||
const { response, error } = await httpGet<{ loginUrl: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-github-login`,
|
||||
@@ -89,6 +94,7 @@ export function GitHubButton(props: GitHubButtonProps) {
|
||||
);
|
||||
|
||||
setIsLoading(false);
|
||||
setIsDisabled?.(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -112,7 +118,7 @@ export function GitHubButton(props: GitHubButtonProps) {
|
||||
<>
|
||||
<button
|
||||
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
disabled={isLoading}
|
||||
disabled={isLoading || isDisabled}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{isLoading ? (
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
|
||||
import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import { GoogleIcon } from '../ReactIcons/GoogleIcon.tsx';
|
||||
|
||||
type GoogleButtonProps = {};
|
||||
type GoogleButtonProps = {
|
||||
isDisabled?: boolean;
|
||||
setIsDisabled?: (isDisabled: boolean) => void;
|
||||
};
|
||||
|
||||
const GOOGLE_REDIRECT_AT = 'googleRedirectAt';
|
||||
const GOOGLE_LAST_PAGE = 'googleLastPage';
|
||||
|
||||
export function GoogleButton(props: GoogleButtonProps) {
|
||||
const { isDisabled, setIsDisabled } = props;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
@@ -25,6 +30,7 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setIsDisabled?.(true);
|
||||
httpGet<{ token: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-google-callback${
|
||||
window.location.search
|
||||
@@ -34,6 +40,7 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
if (!response?.token) {
|
||||
setError(error?.message || 'Something went wrong.');
|
||||
setIsLoading(false);
|
||||
setIsDisabled?.(false);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -62,21 +69,19 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
|
||||
localStorage.removeItem(GOOGLE_REDIRECT_AT);
|
||||
localStorage.removeItem(GOOGLE_LAST_PAGE);
|
||||
Cookies.set(TOKEN_COOKIE_NAME, response.token, {
|
||||
path: '/',
|
||||
expires: 30,
|
||||
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
|
||||
});
|
||||
setAuthToken(response.token);
|
||||
window.location.href = redirectUrl;
|
||||
})
|
||||
.catch((err) => {
|
||||
setError('Something went wrong. Please try again later.');
|
||||
setIsLoading(false);
|
||||
setIsDisabled?.(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleClick = () => {
|
||||
setIsLoading(true);
|
||||
setIsDisabled?.(true);
|
||||
httpGet<{ loginUrl: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-google-login`,
|
||||
)
|
||||
@@ -84,6 +89,7 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
if (!response?.loginUrl) {
|
||||
setError(error?.message || 'Something went wrong.');
|
||||
setIsLoading(false);
|
||||
setIsDisabled?.(false);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -106,6 +112,7 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
.catch((err) => {
|
||||
setError('Something went wrong. Please try again later.');
|
||||
setIsLoading(false);
|
||||
setIsDisabled?.(false);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -113,7 +120,7 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
<>
|
||||
<button
|
||||
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
disabled={isLoading}
|
||||
disabled={isLoading || isDisabled}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{isLoading ? (
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
|
||||
import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import { LinkedInIcon } from '../ReactIcons/LinkedInIcon.tsx';
|
||||
|
||||
type LinkedInButtonProps = {};
|
||||
type LinkedInButtonProps = {
|
||||
isDisabled?: boolean;
|
||||
setIsDisabled?: (isDisabled: boolean) => void;
|
||||
};
|
||||
|
||||
const LINKEDIN_REDIRECT_AT = 'linkedInRedirectAt';
|
||||
const LINKEDIN_LAST_PAGE = 'linkedInLastPage';
|
||||
|
||||
export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
const { isDisabled, setIsDisabled } = props;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
@@ -25,6 +30,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setIsDisabled?.(true);
|
||||
httpGet<{ token: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-linkedin-callback${
|
||||
window.location.search
|
||||
@@ -34,6 +40,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
if (!response?.token) {
|
||||
setError(error?.message || 'Something went wrong.');
|
||||
setIsLoading(false);
|
||||
setIsDisabled?.(false);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -62,21 +69,19 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
|
||||
localStorage.removeItem(LINKEDIN_REDIRECT_AT);
|
||||
localStorage.removeItem(LINKEDIN_LAST_PAGE);
|
||||
Cookies.set(TOKEN_COOKIE_NAME, response.token, {
|
||||
path: '/',
|
||||
expires: 30,
|
||||
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
|
||||
});
|
||||
setAuthToken(response.token);
|
||||
window.location.href = redirectUrl;
|
||||
})
|
||||
.catch((err) => {
|
||||
setError('Something went wrong. Please try again later.');
|
||||
setIsLoading(false);
|
||||
setIsDisabled?.(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleClick = () => {
|
||||
setIsLoading(true);
|
||||
setIsDisabled?.(true);
|
||||
httpGet<{ loginUrl: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-linkedin-login`,
|
||||
)
|
||||
@@ -84,6 +89,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
if (!response?.loginUrl) {
|
||||
setError(error?.message || 'Something went wrong.');
|
||||
setIsLoading(false);
|
||||
setIsDisabled?.(false);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -106,6 +112,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
.catch((err) => {
|
||||
setError('Something went wrong. Please try again later.');
|
||||
setIsLoading(false);
|
||||
setIsDisabled?.(false);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -113,7 +120,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
<>
|
||||
<button
|
||||
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
disabled={isLoading}
|
||||
disabled={isLoading || isDisabled}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{isLoading ? (
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
---
|
||||
import Popup from '../Popup/Popup.astro';
|
||||
import { EmailLoginForm } from './EmailLoginForm';
|
||||
import Divider from './Divider.astro';
|
||||
import { GitHubButton } from './GitHubButton';
|
||||
import { GoogleButton } from './GoogleButton';
|
||||
import { LinkedInButton } from './LinkedInButton';
|
||||
import { AuthenticationForm } from './AuthenticationForm';
|
||||
---
|
||||
|
||||
<Popup id='login-popup' title='' subtitle=''>
|
||||
<div class='text-center'>
|
||||
<div class='mb-7 text-center'>
|
||||
<p class='mb-3 text-2xl font-semibold leading-5 text-slate-900'>
|
||||
Login to your account
|
||||
</p>
|
||||
@@ -16,19 +12,9 @@ import { LinkedInButton } from './LinkedInButton';
|
||||
You must be logged in to perform this action.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class='mt-7 flex flex-col gap-2'>
|
||||
<GitHubButton client:load />
|
||||
<GoogleButton client:load />
|
||||
<LinkedInButton client:load />
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<EmailLoginForm client:load />
|
||||
|
||||
<AuthenticationForm client:load />
|
||||
<div class='mt-6 text-center text-sm text-slate-600'>
|
||||
Don't have an account?{' '}
|
||||
<a href='/signup' class='font-medium text-[#4285f4]'>Sign up</a>
|
||||
<a href='/signup' class='font-medium text-[#4285f4]'> Sign up</a>
|
||||
</div>
|
||||
</Popup>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type FormEvent, useEffect, useState } from 'react';
|
||||
import { httpPost } from '../../lib/http';
|
||||
import Cookies from 'js-cookie';
|
||||
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
|
||||
import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt';
|
||||
|
||||
export function ResetPasswordForm() {
|
||||
const [code, setCode] = useState('');
|
||||
@@ -37,7 +37,7 @@ export function ResetPasswordForm() {
|
||||
newPassword: password,
|
||||
confirmPassword: passwordConfirm,
|
||||
code,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (error?.message) {
|
||||
@@ -53,11 +53,7 @@ export function ResetPasswordForm() {
|
||||
}
|
||||
|
||||
const token = response.token;
|
||||
Cookies.set(TOKEN_COOKIE_NAME, token, {
|
||||
path: '/',
|
||||
expires: 30,
|
||||
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
|
||||
});
|
||||
setAuthToken(response.token);
|
||||
window.location.href = '/';
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
import { httpPost } from '../../lib/http';
|
||||
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
|
||||
import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt';
|
||||
import { Spinner } from '../ReactIcons/Spinner';
|
||||
import { ErrorIcon2 } from '../ReactIcons/ErrorIcon2';
|
||||
|
||||
@@ -26,11 +26,7 @@ export function TriggerVerifyAccount() {
|
||||
return;
|
||||
}
|
||||
|
||||
Cookies.set(TOKEN_COOKIE_NAME, response.token, {
|
||||
path: '/',
|
||||
expires: 30,
|
||||
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
|
||||
});
|
||||
setAuthToken(response.token);
|
||||
window.location.href = '/';
|
||||
})
|
||||
.catch((err) => {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import Cookies from 'js-cookie';
|
||||
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
|
||||
import { TOKEN_COOKIE_NAME, removeAuthToken } from '../../lib/jwt';
|
||||
|
||||
export function logout() {
|
||||
Cookies.remove(TOKEN_COOKIE_NAME, {
|
||||
path: '/',
|
||||
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
|
||||
});
|
||||
removeAuthToken();
|
||||
|
||||
// Reloading will automatically redirect the user if required
|
||||
window.location.reload();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Cookies from 'js-cookie';
|
||||
import { type ChangeEvent, type FormEvent, useEffect, useRef, useState } from 'react';
|
||||
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
|
||||
import { TOKEN_COOKIE_NAME, removeAuthToken } from '../../lib/jwt';
|
||||
|
||||
interface PreviewFile extends File {
|
||||
preview: string;
|
||||
@@ -128,7 +128,7 @@ export default function UploadProfilePicture(props: UploadProfilePictureProps) {
|
||||
|
||||
// Logout user if token is invalid
|
||||
if (data.status === 401) {
|
||||
Cookies.remove(TOKEN_COOKIE_NAME);
|
||||
removeAuthToken();
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
# Hateoas
|
||||
|
||||
HATEOAS is an acronym for <b>H</b>ypermedia <b>A</b>s <b>T</b>he <b>E</b>ngine <b>O</b>f <b>A</b>pplication <b>S</b>tate, it's the concept that when sending information over a RESTful API the document received should contain everything the client needs in order to parse and use the data i.e they don't have to contact any other endpoint not explicitly mentioned within the Document
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [Oktane17: Designing Beautiful REST + JSON APIs (3:56 - 5:57)](https://youtu.be/MiOSzpfP1Ww?t=236)
|
||||
HATEOAS is an acronym for <b>H</b>ypermedia <b>A</b>s <b>T</b>he <b>E</b>ngine <b>O</b>f <b>A</b>pplication <b>S</b>tate, it's the concept that when sending information over a RESTful API the document received should contain everything the client needs in order to parse and use the data i.e they don't have to contact any other endpoint not explicitly mentioned within the Document.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
# Quaternion
|
||||
|
||||
The **quaternion** is a complex number system that extends the concept of rotations in three dimensions. It involves four components: one real and three imaginary parts. Quaternions are used in game development for efficient and accurate calculations of rotations and orientation. They are particularly useful over other methods, such as Euler angles, due to their resistance to problems like Gimbal lock. Despite their complex nature, understanding and implementing quaternions can greatly enhance a game's 3D rotational mechanics and accuracy.
|
||||
The **quaternion** is a complex number system that extends the concept of rotations in three dimensions. It involves four components: one real and three imaginary parts. Quaternions are used in game development for efficient and accurate calculations of rotations and orientation. They are particularly useful over other methods, such as Euler angles, due to their resistance to problems like Gimbal lock. Despite their complex nature, understanding and implementing quaternions can greatly enhance a game's 3D rotational mechanics and accuracy.
|
||||
|
||||
- [Understanding Quaternions](https://www.3dgep.com/understanding-quaternions/)
|
||||
- [Unity docs - Quaternions](https://docs.unity3d.com/ScriptReference/Quaternion.html)
|
||||
- [Quaternions and 3d rotation,explained interactively](https://youtu.be/zjMuIxRvygQ?si=ANmFr5k8JMUzBCUC)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Rust
|
||||
|
||||
**Rust** is a modern, open-source, multi-paradigm programming language designed for performance and safety, especially safe concurrency. It was initially designed by Mozilla Research as a language that can provide memory safety without garbage collection. Since then, it has gained popularity due to its features and performance that often compare favorably to languages like C++. Its rich type system and ownership model guarantee memory-safety and thread-safety while maintaining a high level of abstraction. Rust supports a mixture of imperative procedural, concurrent actor, object-oriented and pure functional styles.
|
||||
**Rust** is a modern, open-source, multi-paradigm programming language designed for performance and safety, especially safe concurrency. It was initially designed by Mozilla Research as a language that can provide memory safety without garbage collection. Since then, it has gained popularity due to its features and performance that often compare favorably to languages like C++. Its rich type system and ownership model guarantee memory-safety and thread-safety while maintaining a high level of abstraction. Rust supports a mixture of imperative procedural, concurrent actor, object-oriented and pure functional styles.
|
||||
|
||||
[Learn Rust full tutorial](https://youtu.be/BpPEoZW5IiY?si=lyBbBPLXQ0HWdJNr
|
||||
|
||||
@@ -70,4 +70,4 @@ db.events.aggregate([
|
||||
|
||||
This query groups events by the day and year, providing a count of events for each day.
|
||||
|
||||
- [MongoDB Documentation Date](https://www.mongodb.com/docs/manual/reference/method/date/)
|
||||
- [MongoDB Documentation Date](https://www.mongodb.com/docs/manual/reference/method/Date/)
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
# Analyzer
|
||||
# MongoDB Analyzer
|
||||
|
||||
The Visual Studio (VS) Analyzer for MongoDB is a powerful development tool that helps you work with MongoDB by providing an integrated environment within your Visual Studio IDE. This add-on enhances your productivity and efficiency when developing applications with MongoDB, as it offers several benefits such as code assistance, syntax highlighting, IntelliSense support, and more.
|
||||
The MongoDB Analyzer for Visual Studio (VS) is a powerful development tool that helps you work with MongoDB by providing an integrated environment within your Visual Studio IDE. This add-on enhances your productivity and efficiency when developing applications with MongoDB, as it offers several benefits such as code assistance, syntax highlighting, IntelliSense support, and more.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Syntax Highlighting**: The VS Analyzer provides syntax highlighting to help you quickly identify and understand different elements in your code, such as variables, operators, and functions.
|
||||
- **Syntax Highlighting**: The MongoDB Analyzer provides syntax highlighting to help you quickly identify and understand different elements in your code, such as variables, operators, and functions.
|
||||
|
||||
- **IntelliSense Support**: IntelliSense is an intelligent code completion feature that predicts and suggests likely entries based on the context. It makes it easier to write queries by providing contextual suggestions based on your input.
|
||||
|
||||
- **Code Snippets**: This feature allows you to insert common MongoDB code patterns and functionalities directly into your code editor with just a few clicks. This can save you time and help maintain a consistent coding style across your project.
|
||||
|
||||
- **Query Profiling**: The VS Analyzer allows you to profile and optimize MongoDB queries. By analyzing query performance, you can identify slow or problematic queries and make appropriate improvements to ensure better performance.
|
||||
- **Query Profiling**: The MongoDB Analyzer allows you to profile and optimize MongoDB queries. By analyzing query performance, you can identify slow or problematic queries and make appropriate improvements to ensure better performance.
|
||||
|
||||
- **Debugging**: The Analyzer offers debugging support to help you identify and fix issues in your MongoDB queries and scripts, improving the overall reliability of your application.
|
||||
|
||||
- **Integrated Shell**: VS Analyzer offers an integrated shell within Visual Studio that allows you to run MongoDB commands and queries directly within the IDE. This makes it more convenient to interact with your MongoDB instances and perform various tasks without switching between different tools.
|
||||
- **Integrated Shell**: MongoDB Analyzer offers an integrated shell within Visual Studio that allows you to run MongoDB commands and queries directly within the IDE. This makes it more convenient to interact with your MongoDB instances and perform various tasks without switching between different tools.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To start using the VS Analyzer for MongoDB, follow these steps:
|
||||
|
||||
- Download and install the [Visual Studio MongoDB Extension](https://marketplace.visualstudio.com/items?itemName=ms-ossdata.vscode-mongodb) from the Visual Studio Marketplace.
|
||||
To start using MongoDB Analyzer for Visual Studio, follow these steps:
|
||||
|
||||
- Open your Visual Studio IDE and create a new project or open an existing one.
|
||||
|
||||
- Add a reference to the MongoDB extension in your project by right-clicking on `References` and selecting `Add Package`.
|
||||
- Download and install the [MongoDB Analyzer as a NuGet package](https://www.nuget.org/packages/MongoDB.Analyzer/1.0.0) in Visual Studio from:
|
||||
|
||||
- Search for `MongoDB` in the package manager window, and install the relevant packages for your project.
|
||||
- **Package Manager**: Click `Tools` > `NuGet Package Manager` > `Package Manager Console` and then execute this command: ```Install-Package MongoDB.Analyzer -Version 1.0.0```
|
||||
- **.NET CLI**: Click `View` > `Terminal` and then execute this command: ```dotnet add package MongoDB.Analyzer --version 1.0.0```
|
||||
|
||||
- Once the extension is installed, you can access the MongoDB features through the `Extensions` menu in Visual Studio.
|
||||
- Once installed, it will be added to your project’s Dependencies list, under Analyzers.
|
||||
|
||||
With the VS Analyzer for MongoDB, you'll be able to write cleaner, faster, and more efficient code, making it an essential tool for any MongoDB developer.
|
||||
- After installing and once the analyzer has run, you’ll find all of the diagnostic warnings output to the Error List panel. As you start to inspect your code, you’ll also see that any unsupported expressions will be highlighted.
|
||||
|
||||
- Learn more about MongoDB Analyzer from the [official docs](https://www.mongodb.com/developer/languages/csharp/introducing-mongodb-analyzer-dotnet/).
|
||||
|
||||
With the MongoDB Analyzer for Visual Studio, you'll be able to write cleaner, faster, and more efficient code, making it an essential tool for any MongoDB developer.
|
||||
|
||||
@@ -4,5 +4,5 @@ Routing is an essential concept in Single Page Applications (SPA). When your app
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [How to use Routing in React JS: A Comprehensive Guide. ](https://teachingbee.in/how-to-use-routing-in-react-js/)
|
||||
- [How to use Routing in React JS: A Comprehensive Guide. ](https://blog.logrocket.com/react-router-v6-guide/)
|
||||
- [React Router 6 – Tutorial for Beginners. ](https://www.youtube.com/watch?v=59IXY5IDrBA)
|
||||
|
||||
@@ -12,4 +12,4 @@ DDD connects the implementation to an evolving model and it is predicated on the
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [Domain Driven Design Quickly](https://matfrs2.github.io/RS2/predavanja/literatura/Avram%20A,%20Marinescu%20F.%20-%20Domain%20Driven%20Design%20Quickly.pdf)
|
||||
- [Domain Driven Design Quickly](https://web.archive.org/web/20230606035225/https://matfrs2.github.io/RS2/predavanja/literatura/Avram%20A,%20Marinescu%20F.%20-%20Domain%20Driven%20Design%20Quickly.pdf)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Cookies from 'js-cookie';
|
||||
import fp from '@fingerprintjs/fingerprintjs';
|
||||
import { TOKEN_COOKIE_NAME } from './jwt';
|
||||
import { TOKEN_COOKIE_NAME, removeAuthToken } from './jwt';
|
||||
|
||||
type HttpOptionsType = RequestInit | { headers: Record<string, any> };
|
||||
|
||||
@@ -30,10 +30,10 @@ type ApiReturn<ResponseType, ErrorType> = {
|
||||
*/
|
||||
export async function httpCall<
|
||||
ResponseType = AppResponse,
|
||||
ErrorType = AppError
|
||||
ErrorType = AppError,
|
||||
>(
|
||||
url: string,
|
||||
options?: HttpOptionsType
|
||||
options?: HttpOptionsType,
|
||||
): Promise<ApiReturn<ResponseType, ErrorType>> {
|
||||
try {
|
||||
const fingerprintPromise = await fp.load({ monitoring: false });
|
||||
@@ -65,7 +65,7 @@ export async function httpCall<
|
||||
|
||||
// Logout user if token is invalid
|
||||
if (data.status === 401) {
|
||||
Cookies.remove(TOKEN_COOKIE_NAME);
|
||||
removeAuthToken();
|
||||
window.location.reload();
|
||||
return { response: undefined, error: data as ErrorType };
|
||||
}
|
||||
@@ -92,11 +92,11 @@ export async function httpCall<
|
||||
|
||||
export async function httpPost<
|
||||
ResponseType = AppResponse,
|
||||
ErrorType = AppError
|
||||
ErrorType = AppError,
|
||||
>(
|
||||
url: string,
|
||||
body: Record<string, any>,
|
||||
options?: HttpOptionsType
|
||||
options?: HttpOptionsType,
|
||||
): Promise<ApiReturn<ResponseType, ErrorType>> {
|
||||
return httpCall<ResponseType, ErrorType>(url, {
|
||||
...options,
|
||||
@@ -108,7 +108,7 @@ export async function httpPost<
|
||||
export async function httpGet<ResponseType = AppResponse, ErrorType = AppError>(
|
||||
url: string,
|
||||
queryParams?: Record<string, any>,
|
||||
options?: HttpOptionsType
|
||||
options?: HttpOptionsType,
|
||||
): Promise<ApiReturn<ResponseType, ErrorType>> {
|
||||
const searchParams = new URLSearchParams(queryParams).toString();
|
||||
const queryUrl = searchParams ? `${url}?${searchParams}` : url;
|
||||
@@ -122,11 +122,11 @@ export async function httpGet<ResponseType = AppResponse, ErrorType = AppError>(
|
||||
|
||||
export async function httpPatch<
|
||||
ResponseType = AppResponse,
|
||||
ErrorType = AppError
|
||||
ErrorType = AppError,
|
||||
>(
|
||||
url: string,
|
||||
body: Record<string, any>,
|
||||
options?: HttpOptionsType
|
||||
options?: HttpOptionsType,
|
||||
): Promise<ApiReturn<ResponseType, ErrorType>> {
|
||||
return httpCall<ResponseType, ErrorType>(url, {
|
||||
...options,
|
||||
@@ -138,7 +138,7 @@ export async function httpPatch<
|
||||
export async function httpPut<ResponseType = AppResponse, ErrorType = AppError>(
|
||||
url: string,
|
||||
body: Record<string, any>,
|
||||
options?: HttpOptionsType
|
||||
options?: HttpOptionsType,
|
||||
): Promise<ApiReturn<ResponseType, ErrorType>> {
|
||||
return httpCall<ResponseType, ErrorType>(url, {
|
||||
...options,
|
||||
@@ -149,10 +149,10 @@ export async function httpPut<ResponseType = AppResponse, ErrorType = AppError>(
|
||||
|
||||
export async function httpDelete<
|
||||
ResponseType = AppResponse,
|
||||
ErrorType = AppError
|
||||
ErrorType = AppError,
|
||||
>(
|
||||
url: string,
|
||||
options?: HttpOptionsType
|
||||
options?: HttpOptionsType,
|
||||
): Promise<ApiReturn<ResponseType, ErrorType>> {
|
||||
return httpCall<ResponseType, ErrorType>(url, {
|
||||
...options,
|
||||
|
||||
@@ -31,3 +31,20 @@ export function getUser() {
|
||||
|
||||
return decodeToken(token);
|
||||
}
|
||||
|
||||
export function setAuthToken(token: string) {
|
||||
Cookies.set(TOKEN_COOKIE_NAME, token, {
|
||||
path: '/',
|
||||
expires: 30,
|
||||
sameSite: 'lax',
|
||||
secure: true,
|
||||
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
|
||||
});
|
||||
}
|
||||
|
||||
export function removeAuthToken() {
|
||||
Cookies.remove(TOKEN_COOKIE_NAME, {
|
||||
path: '/',
|
||||
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
---
|
||||
import Divider from '../components/AuthenticationFlow/Divider.astro';
|
||||
import { EmailLoginForm } from '../components/AuthenticationFlow/EmailLoginForm';
|
||||
import { GitHubButton } from '../components/AuthenticationFlow/GitHubButton';
|
||||
import { GoogleButton } from '../components/AuthenticationFlow/GoogleButton';
|
||||
import { LinkedInButton } from '../components/AuthenticationFlow/LinkedInButton';
|
||||
import { AuthenticationForm } from '../components/AuthenticationFlow/AuthenticationForm';
|
||||
import AccountLayout from '../layouts/AccountLayout.astro';
|
||||
---
|
||||
|
||||
@@ -14,23 +10,17 @@ import AccountLayout from '../layouts/AccountLayout.astro';
|
||||
noIndex={true}
|
||||
>
|
||||
<div class='container'>
|
||||
<div class='mx-auto flex flex-col items-start justify-start pb-28 pt-10 sm:max-w-[400px] sm:items-center sm:justify-center sm:pt-20'>
|
||||
<div
|
||||
class='mx-auto flex flex-col items-start justify-start pb-28 pt-10 sm:max-w-[400px] sm:items-center sm:justify-center sm:pt-20'
|
||||
>
|
||||
<div class='mb-2 text-left sm:mb-5 sm:text-center'>
|
||||
<h1 class='mb-2 text-3xl font-semibold sm:mb-5 sm:text-5xl'>Login</h1>
|
||||
<p class='text-base text-gray-600 leading-6 mb-3'>
|
||||
<p class='mb-3 text-base leading-6 text-gray-600'>
|
||||
Welcome back! Let's take you to your account.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class='flex w-full flex-col gap-2'>
|
||||
<GitHubButton client:load />
|
||||
<GoogleButton client:load />
|
||||
<LinkedInButton client:load />
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<EmailLoginForm client:load />
|
||||
<AuthenticationForm client:load />
|
||||
|
||||
<div class='mt-6 text-center text-sm text-slate-600'>
|
||||
Don't have an account?{' '}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
---
|
||||
import Divider from '../components/AuthenticationFlow/Divider.astro';
|
||||
import { EmailSignupForm } from '../components/AuthenticationFlow/EmailSignupForm';
|
||||
import { GitHubButton } from '../components/AuthenticationFlow/GitHubButton';
|
||||
import { GoogleButton } from '../components/AuthenticationFlow/GoogleButton';
|
||||
import { LinkedInButton } from '../components/AuthenticationFlow/LinkedInButton';
|
||||
import { AuthenticationForm } from '../components/AuthenticationFlow/AuthenticationForm';
|
||||
import AccountLayout from '../layouts/AccountLayout.astro';
|
||||
---
|
||||
|
||||
@@ -29,15 +25,7 @@ import AccountLayout from '../layouts/AccountLayout.astro';
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class='flex w-full flex-col items-stretch gap-2'>
|
||||
<GitHubButton client:load />
|
||||
<GoogleButton client:load />
|
||||
<LinkedInButton client:load />
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<EmailSignupForm client:load />
|
||||
<AuthenticationForm type='signup' client:load />
|
||||
|
||||
<div class='mt-6 text-center text-sm text-slate-600'>
|
||||
Already have an account? <a
|
||||
|
||||
Reference in New Issue
Block a user