mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-09-09 00:30:40 +02:00
feat: new user flag (#8070)
* feat: new user flag * feat: share icon event * fix: upload the query tag * fix: name and label --------- Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import Cookies from 'js-cookie';
|
||||
import type { FormEvent } from 'react';
|
||||
import { useId, useState } from 'react';
|
||||
import { httpPost } from '../../lib/http';
|
||||
import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt';
|
||||
import { FIRST_LOGIN_PARAM, setAuthToken } from '../../lib/jwt';
|
||||
|
||||
type EmailLoginFormProps = {
|
||||
isDisabled?: boolean;
|
||||
@@ -24,19 +24,24 @@ export function EmailLoginForm(props: EmailLoginFormProps) {
|
||||
setIsDisabled?.(true);
|
||||
setError('');
|
||||
|
||||
const { response, error } = await httpPost<{ token: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-login`,
|
||||
{
|
||||
email,
|
||||
password,
|
||||
},
|
||||
);
|
||||
const { response, error } = await httpPost<{
|
||||
token: string;
|
||||
isNewUser: boolean;
|
||||
}>(`${import.meta.env.PUBLIC_API_URL}/v1-login`, {
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
// Log the user in and reload the page
|
||||
if (response?.token) {
|
||||
setAuthToken(response.token);
|
||||
window.location.reload();
|
||||
|
||||
const currentLocation = window.location.href;
|
||||
const url = new URL(currentLocation, window.location.origin);
|
||||
if (response?.isNewUser) {
|
||||
url.searchParams.set(FIRST_LOGIN_PARAM, '1');
|
||||
}
|
||||
window.location.href = url.toString();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -1,8 +1,12 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
|
||||
import {
|
||||
FIRST_LOGIN_PARAM,
|
||||
COURSE_PURCHASE_PARAM,
|
||||
setAuthToken,
|
||||
} from '../../lib/jwt';
|
||||
import { cn } from '../../../editor/utils/classname.ts';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { COURSE_PURCHASE_PARAM, setAuthToken } from '../../lib/jwt';
|
||||
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx';
|
||||
import { triggerUtmRegistration } from '../../lib/browser.ts';
|
||||
@@ -34,7 +38,7 @@ export function GitHubButton(props: GitHubButtonProps) {
|
||||
|
||||
setIsLoading(true);
|
||||
setIsDisabled?.(true);
|
||||
httpGet<{ token: string }>(
|
||||
httpGet<{ token: string; isNewUser: boolean }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-github-callback${
|
||||
window.location.search
|
||||
}`,
|
||||
@@ -51,7 +55,7 @@ export function GitHubButton(props: GitHubButtonProps) {
|
||||
|
||||
triggerUtmRegistration();
|
||||
|
||||
let redirectUrl = '/';
|
||||
let redirectUrl = new URL('/', window.location.origin);
|
||||
const gitHubRedirectAt = localStorage.getItem(GITHUB_REDIRECT_AT);
|
||||
const lastPageBeforeGithub = localStorage.getItem(GITHUB_LAST_PAGE);
|
||||
|
||||
@@ -63,31 +67,36 @@ export function GitHubButton(props: GitHubButtonProps) {
|
||||
const timeSinceRedirect = now - socialRedirectAtTime;
|
||||
|
||||
if (timeSinceRedirect < 30 * 1000) {
|
||||
redirectUrl = lastPageBeforeGithub;
|
||||
redirectUrl = new URL(lastPageBeforeGithub, window.location.origin);
|
||||
}
|
||||
}
|
||||
|
||||
const authRedirectUrl = localStorage.getItem('authRedirect');
|
||||
if (authRedirectUrl) {
|
||||
localStorage.removeItem('authRedirect');
|
||||
redirectUrl = authRedirectUrl;
|
||||
redirectUrl = new URL(authRedirectUrl, window.location.origin);
|
||||
}
|
||||
|
||||
localStorage.removeItem(GITHUB_REDIRECT_AT);
|
||||
localStorage.removeItem(GITHUB_LAST_PAGE);
|
||||
setAuthToken(response.token);
|
||||
|
||||
if (response?.isNewUser) {
|
||||
redirectUrl.searchParams.set(FIRST_LOGIN_PARAM, '1');
|
||||
}
|
||||
|
||||
const shouldTriggerPurchase =
|
||||
localStorage.getItem(CHECKOUT_AFTER_LOGIN_KEY) !== '0';
|
||||
if (redirectUrl.includes('/courses/sql') && shouldTriggerPurchase) {
|
||||
const tempUrl = new URL(redirectUrl, window.location.origin);
|
||||
tempUrl.searchParams.set(COURSE_PURCHASE_PARAM, '1');
|
||||
redirectUrl = tempUrl.toString();
|
||||
|
||||
if (
|
||||
redirectUrl.pathname.includes('/courses/sql') &&
|
||||
shouldTriggerPurchase
|
||||
) {
|
||||
redirectUrl.searchParams.set(COURSE_PURCHASE_PARAM, '1');
|
||||
localStorage.removeItem(CHECKOUT_AFTER_LOGIN_KEY);
|
||||
}
|
||||
|
||||
window.location.href = redirectUrl;
|
||||
window.location.href = redirectUrl.toString();
|
||||
})
|
||||
.catch((err) => {
|
||||
setError('Something went wrong. Please try again later.');
|
||||
|
@@ -1,7 +1,12 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import Cookies from 'js-cookie';
|
||||
import {
|
||||
FIRST_LOGIN_PARAM,
|
||||
TOKEN_COOKIE_NAME,
|
||||
setAuthToken,
|
||||
} from '../../lib/jwt';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { COURSE_PURCHASE_PARAM, setAuthToken } from '../../lib/jwt';
|
||||
import { COURSE_PURCHASE_PARAM } from '../../lib/jwt';
|
||||
import { GoogleIcon } from '../ReactIcons/GoogleIcon.tsx';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx';
|
||||
@@ -9,6 +14,7 @@ import {
|
||||
getStoredUtmParams,
|
||||
triggerUtmRegistration,
|
||||
} from '../../lib/browser.ts';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
|
||||
type GoogleButtonProps = {
|
||||
isDisabled?: boolean;
|
||||
@@ -37,14 +43,12 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
|
||||
setIsLoading(true);
|
||||
setIsDisabled?.(true);
|
||||
httpGet<{ token: string }>(
|
||||
httpGet<{ token: string; isNewUser: boolean }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-google-callback${
|
||||
window.location.search
|
||||
}`,
|
||||
)
|
||||
.then(({ response, error }) => {
|
||||
const utmParams = getStoredUtmParams();
|
||||
|
||||
if (!response?.token) {
|
||||
setError(error?.message || 'Something went wrong.');
|
||||
setIsLoading(false);
|
||||
@@ -55,7 +59,7 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
|
||||
triggerUtmRegistration();
|
||||
|
||||
let redirectUrl = '/';
|
||||
let redirectUrl = new URL('/', window.location.origin);
|
||||
const googleRedirectAt = localStorage.getItem(GOOGLE_REDIRECT_AT);
|
||||
const lastPageBeforeGoogle = localStorage.getItem(GOOGLE_LAST_PAGE);
|
||||
|
||||
@@ -67,22 +71,27 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
const timeSinceRedirect = now - socialRedirectAtTime;
|
||||
|
||||
if (timeSinceRedirect < 30 * 1000) {
|
||||
redirectUrl = lastPageBeforeGoogle;
|
||||
redirectUrl = new URL(lastPageBeforeGoogle, window.location.origin);
|
||||
}
|
||||
}
|
||||
|
||||
const authRedirectUrl = localStorage.getItem('authRedirect');
|
||||
if (authRedirectUrl) {
|
||||
localStorage.removeItem('authRedirect');
|
||||
redirectUrl = authRedirectUrl;
|
||||
redirectUrl = new URL(authRedirectUrl, window.location.origin);
|
||||
}
|
||||
|
||||
if (response?.isNewUser) {
|
||||
redirectUrl.searchParams.set(FIRST_LOGIN_PARAM, '1');
|
||||
}
|
||||
|
||||
const shouldTriggerPurchase =
|
||||
localStorage.getItem(CHECKOUT_AFTER_LOGIN_KEY) !== '0';
|
||||
if (redirectUrl.includes('/courses/sql') && shouldTriggerPurchase) {
|
||||
const tempUrl = new URL(redirectUrl, window.location.origin);
|
||||
tempUrl.searchParams.set(COURSE_PURCHASE_PARAM, '1');
|
||||
redirectUrl = tempUrl.toString();
|
||||
if (
|
||||
redirectUrl.pathname.includes('/courses/sql') &&
|
||||
shouldTriggerPurchase
|
||||
) {
|
||||
redirectUrl.searchParams.set(COURSE_PURCHASE_PARAM, '1');
|
||||
|
||||
localStorage.removeItem(CHECKOUT_AFTER_LOGIN_KEY);
|
||||
}
|
||||
@@ -90,7 +99,8 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
localStorage.removeItem(GOOGLE_REDIRECT_AT);
|
||||
localStorage.removeItem(GOOGLE_LAST_PAGE);
|
||||
setAuthToken(response.token);
|
||||
window.location.href = redirectUrl;
|
||||
|
||||
window.location.href = redirectUrl.toString();
|
||||
})
|
||||
.catch((err) => {
|
||||
setError('Something went wrong. Please try again later.');
|
||||
|
@@ -1,7 +1,13 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
import {
|
||||
FIRST_LOGIN_PARAM,
|
||||
COURSE_PURCHASE_PARAM,
|
||||
TOKEN_COOKIE_NAME,
|
||||
setAuthToken,
|
||||
} from '../../lib/jwt';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { COURSE_PURCHASE_PARAM, setAuthToken } from '../../lib/jwt';
|
||||
import { LinkedInIcon } from '../ReactIcons/LinkedInIcon.tsx';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx';
|
||||
@@ -34,7 +40,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
|
||||
setIsLoading(true);
|
||||
setIsDisabled?.(true);
|
||||
httpGet<{ token: string }>(
|
||||
httpGet<{ token: string; isNewUser: boolean }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-linkedin-callback${
|
||||
window.location.search
|
||||
}`,
|
||||
@@ -50,7 +56,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
|
||||
triggerUtmRegistration();
|
||||
|
||||
let redirectUrl = '/';
|
||||
let redirectUrl = new URL('/', window.location.origin);
|
||||
const linkedInRedirectAt = localStorage.getItem(LINKEDIN_REDIRECT_AT);
|
||||
const lastPageBeforeLinkedIn = localStorage.getItem(LINKEDIN_LAST_PAGE);
|
||||
|
||||
@@ -62,30 +68,38 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
const timeSinceRedirect = now - socialRedirectAtTime;
|
||||
|
||||
if (timeSinceRedirect < 30 * 1000) {
|
||||
redirectUrl = lastPageBeforeLinkedIn;
|
||||
redirectUrl = new URL(
|
||||
lastPageBeforeLinkedIn,
|
||||
window.location.origin,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const authRedirectUrl = localStorage.getItem('authRedirect');
|
||||
if (authRedirectUrl) {
|
||||
localStorage.removeItem('authRedirect');
|
||||
redirectUrl = authRedirectUrl;
|
||||
redirectUrl = new URL(authRedirectUrl, window.location.origin);
|
||||
}
|
||||
|
||||
if (response?.isNewUser) {
|
||||
redirectUrl.searchParams.set(FIRST_LOGIN_PARAM, '1');
|
||||
}
|
||||
|
||||
const shouldTriggerPurchase =
|
||||
localStorage.getItem(CHECKOUT_AFTER_LOGIN_KEY) !== '0';
|
||||
if (redirectUrl.includes('/courses/sql') && shouldTriggerPurchase) {
|
||||
const tempUrl = new URL(redirectUrl, window.location.origin);
|
||||
tempUrl.searchParams.set(COURSE_PURCHASE_PARAM, '1');
|
||||
redirectUrl = tempUrl.toString();
|
||||
|
||||
if (
|
||||
redirectUrl.pathname.includes('/courses/sql') &&
|
||||
shouldTriggerPurchase
|
||||
) {
|
||||
redirectUrl.searchParams.set(COURSE_PURCHASE_PARAM, '1');
|
||||
localStorage.removeItem(CHECKOUT_AFTER_LOGIN_KEY);
|
||||
}
|
||||
|
||||
localStorage.removeItem(LINKEDIN_REDIRECT_AT);
|
||||
localStorage.removeItem(LINKEDIN_LAST_PAGE);
|
||||
setAuthToken(response.token);
|
||||
window.location.href = redirectUrl;
|
||||
|
||||
window.location.href = redirectUrl.toString();
|
||||
})
|
||||
.catch((err) => {
|
||||
setError('Something went wrong. Please try again later.');
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
import { httpPost } from '../../lib/http';
|
||||
import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt';
|
||||
import { FIRST_LOGIN_PARAM, TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt';
|
||||
import { Spinner } from '../ReactIcons/Spinner';
|
||||
import { ErrorIcon2 } from '../ReactIcons/ErrorIcon2';
|
||||
import { triggerUtmRegistration } from '../../lib/browser.ts';
|
||||
@@ -13,7 +13,7 @@ export function TriggerVerifyAccount() {
|
||||
const triggerVerify = (code: string) => {
|
||||
setIsLoading(true);
|
||||
|
||||
httpPost<{ token: string }>(
|
||||
httpPost<{ token: string; isNewUser: boolean }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-verify-account`,
|
||||
{
|
||||
code,
|
||||
@@ -30,7 +30,12 @@ export function TriggerVerifyAccount() {
|
||||
triggerUtmRegistration();
|
||||
|
||||
setAuthToken(response.token);
|
||||
window.location.href = '/';
|
||||
|
||||
const url = new URL('/', window.location.origin);
|
||||
if (response?.isNewUser) {
|
||||
url.searchParams.set(FIRST_LOGIN_PARAM, '1');
|
||||
}
|
||||
window.location.href = url.toString();
|
||||
})
|
||||
.catch((err) => {
|
||||
setIsLoading(false);
|
||||
|
20
src/components/ReactIcons/HackerNewsIcon.tsx
Normal file
20
src/components/ReactIcons/HackerNewsIcon.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
interface HackerNewsIconProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function HackerNewsIcon(props: HackerNewsIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 448 512"
|
||||
fill="currentColor"
|
||||
className={cn('h-[26px] w-[26px]', className)}
|
||||
>
|
||||
<path d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zM21.2 229.2H21c.1-.1.2-.3.3-.4 0 .1 0 .3-.1.4zm218 53.9V384h-31.4V281.3L128 128h37.3c52.5 98.3 49.2 101.2 59.3 125.6 12.3-27 5.8-24.4 60.6-125.6H320l-80.8 155.1z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
20
src/components/ReactIcons/RedditIcon.tsx
Normal file
20
src/components/ReactIcons/RedditIcon.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
interface RedditIconProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function RedditIcon(props: RedditIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 448 512"
|
||||
fill="currentColor"
|
||||
className={cn('h-[26px] w-[26px]', className)}
|
||||
>
|
||||
<path d="M283.2 345.5c2.7 2.7 2.7 6.8 0 9.2-24.5 24.5-93.8 24.6-118.4 0-2.7-2.4-2.7-6.5 0-9.2 2.4-2.4 6.5-2.4 8.9 0 18.7 19.2 81 19.6 100.5 0 2.4-2.3 6.6-2.3 9 0zm-91.3-53.8c0-14.9-11.9-26.8-26.5-26.8a26.67 26.67 0 0 0-26.8 26.8c0 14.6 11.9 26.5 26.8 26.5 14.6 0 26.5-11.9 26.5-26.5zm90.7-26.8c-14.6 0-26.5 11.9-26.5 26.8 0 14.6 11.9 26.5 26.5 26.5 14.9 0 26.8-11.9 26.8-26.5a26.67 26.67 0 0 0-26.8-26.8zM448 80v352c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V80c0-26.5 21.5-48 48-48h352c26.5 0 48 21.5 48 48zm-99.7 140.6c-10.1 0-19 4.2-25.6 10.7-24.1-16.7-56.5-27.4-92.5-28.6l18.7-84.2 59.5 13.4c0 14.6 11.9 26.5 26.5 26.5 14.9 0 26.8-12.2 26.8-26.8s-11.9-26.8-26.8-26.8c-10.4 0-19.3 6.2-23.8 14.9l-65.7-14.6c-3.3-.9-6.5 1.5-7.4 4.8l-20.5 92.8c-35.7 1.5-67.8 12.2-91.9 28.9-6.5-6.8-15.8-11-25.9-11-37.5 0-49.8 50.4-15.5 67.5-1.2 5.4-1.8 11-1.8 16.7 0 56.5 63.7 102.3 141.9 102.3 78.5 0 142.2-45.8 142.2-102.3 0-5.7-.6-11.6-2.1-17 33.6-17.2 21.2-67.2-16.1-67.2z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
@@ -18,7 +18,7 @@ export function TwitterIcon(props: TwitterIconProps) {
|
||||
<rect width="23" height="23" rx="3" fill={boxColor} />
|
||||
<path
|
||||
d="M12.9285 10.3522L18.5135 4H17.1905L12.339 9.5144L8.467 4H4L9.8565 12.3395L4 19H5.323L10.443 13.1754L14.533 19H19M5.8005 4.97619H7.833L17.1895 18.0718H15.1565"
|
||||
fill='currentColor'
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
@@ -1,34 +0,0 @@
|
||||
---
|
||||
import Icon from '../AstroIcon.astro';
|
||||
|
||||
export interface Props {
|
||||
pageUrl: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const { pageUrl, description } = Astro.props;
|
||||
|
||||
const twitterUrl = `https://twitter.com/intent/tweet?text=${description}&url=${pageUrl}`;
|
||||
const fbUrl = `https://www.facebook.com/sharer/sharer.php?quote=${description}&u=${pageUrl}`;
|
||||
const hnUrl = `https://news.ycombinator.com/submitlink?t=${description}&u=${pageUrl}`;
|
||||
const redditUrl = `https://www.reddit.com/submit?title=${description}&url=${pageUrl}`;
|
||||
---
|
||||
|
||||
<div class='absolute left-[-18px] top-[110px] h-full hidden' id='page-share-icons'>
|
||||
<div class='flex sticky top-[100px] flex-col gap-1.5 items-center'>
|
||||
<a href={twitterUrl} target='_blank' class='text-gray-500 hover:text-gray-700 mb-0.5'>
|
||||
<Icon icon='twitter' />
|
||||
</a>
|
||||
<a href={fbUrl} target='_blank' class='text-gray-500 hover:text-gray-700'>
|
||||
<Icon icon='facebook' />
|
||||
</a>
|
||||
<a href={hnUrl} target='_blank' class='text-gray-500 hover:text-gray-700'>
|
||||
<Icon icon='hackernews' />
|
||||
</a>
|
||||
<a href={redditUrl} target='_blank' class='text-gray-500 hover:text-gray-700'>
|
||||
<Icon icon='reddit' />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src='./sharer.js'></script>
|
105
src/components/ShareIcons/ShareIcons.tsx
Normal file
105
src/components/ShareIcons/ShareIcons.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { FacebookIcon } from '../ReactIcons/FacebookIcon';
|
||||
import { HackerNewsIcon } from '../ReactIcons/HackerNewsIcon';
|
||||
import { RedditIcon } from '../ReactIcons/RedditIcon';
|
||||
import { TwitterIcon } from '../ReactIcons/TwitterIcon';
|
||||
|
||||
type ShareIconsProps = {
|
||||
resourceId: string;
|
||||
resourceType: string;
|
||||
pageUrl: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export function ShareIcons(props: ShareIconsProps) {
|
||||
const { pageUrl, description, resourceType, resourceId } = props;
|
||||
|
||||
const shareIconsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const twitterUrl = `https://twitter.com/intent/tweet?text=${description}&url=${pageUrl}`;
|
||||
const fbUrl = `https://www.facebook.com/sharer/sharer.php?quote=${description}&u=${pageUrl}`;
|
||||
const hnUrl = `https://news.ycombinator.com/submitlink?t=${description}&u=${pageUrl}`;
|
||||
const redditUrl = `https://www.reddit.com/submit?title=${description}&url=${pageUrl}`;
|
||||
|
||||
const icons = [
|
||||
{
|
||||
url: twitterUrl,
|
||||
icon: (
|
||||
<TwitterIcon
|
||||
className="size-[24px] [&>path]:fill-[#E5E5E5]"
|
||||
boxColor="currentColor"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
url: fbUrl,
|
||||
icon: <FacebookIcon className="size-[26px]" />,
|
||||
},
|
||||
{
|
||||
url: hnUrl,
|
||||
icon: <HackerNewsIcon className="size-[26px]" />,
|
||||
},
|
||||
{
|
||||
url: redditUrl,
|
||||
icon: <RedditIcon className="size-[26px]" />,
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const shareIcons = shareIconsRef.current;
|
||||
if (!shareIcons) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onScroll = () => {
|
||||
if (window.scrollY < 100 || window.innerWidth < 1050) {
|
||||
shareIcons.classList.add('hidden');
|
||||
return null;
|
||||
}
|
||||
|
||||
shareIcons.classList.remove('hidden');
|
||||
};
|
||||
|
||||
onScroll();
|
||||
|
||||
window.addEventListener('scroll', onScroll);
|
||||
return () => {
|
||||
window.removeEventListener('scroll', onScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute left-[-18px] top-[110px] hidden h-full"
|
||||
ref={shareIconsRef}
|
||||
>
|
||||
<div className="sticky top-[100px] flex flex-col items-center gap-1.5">
|
||||
{icons.map((icon, index) => {
|
||||
const host = new URL(icon.url).host;
|
||||
|
||||
return (
|
||||
<a
|
||||
key={index}
|
||||
href={icon.url}
|
||||
target="_blank"
|
||||
className={cn(
|
||||
'text-gray-500 hover:text-gray-700',
|
||||
index === 0 && 'mt-0.5',
|
||||
)}
|
||||
onClick={() => {
|
||||
window.fireEvent({
|
||||
category: 'RoadmapShareLink',
|
||||
action: `Share Roadmap / ${resourceType} / ${resourceId} / ${host}`,
|
||||
label: icon.url,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{icon.icon}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1,32 +0,0 @@
|
||||
export class Sharer {
|
||||
constructor() {
|
||||
this.init = this.init.bind(this);
|
||||
this.onScroll = this.onScroll.bind(this);
|
||||
|
||||
this.shareIconsId = 'page-share-icons';
|
||||
}
|
||||
|
||||
get shareIconsEl() {
|
||||
return document.getElementById(this.shareIconsId);
|
||||
}
|
||||
|
||||
onScroll() {
|
||||
if (window.scrollY < 100 || window.innerWidth < 1050) {
|
||||
this.shareIconsEl.classList.add('hidden');
|
||||
return null;
|
||||
}
|
||||
|
||||
this.shareIconsEl.classList.remove('hidden');
|
||||
}
|
||||
|
||||
init() {
|
||||
if (!this.shareIconsEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', this.onScroll, { passive: true });
|
||||
}
|
||||
}
|
||||
|
||||
const sharer = new Sharer();
|
||||
sharer.init();
|
@@ -3,6 +3,7 @@ import Cookies from 'js-cookie';
|
||||
import type { AllowedOnboardingStatus } from '../api/user';
|
||||
|
||||
export const TOKEN_COOKIE_NAME = '__roadmapsh_jt__';
|
||||
export const FIRST_LOGIN_PARAM = 'fl' as const;
|
||||
export const COURSE_PURCHASE_PARAM = 't';
|
||||
|
||||
export type TokenPayload = {
|
||||
|
@@ -1,25 +1,8 @@
|
||||
---
|
||||
import { EditorRoadmap } from '../../components/EditorRoadmap/EditorRoadmap';
|
||||
import FAQs, { type FAQType } from '../../components/FAQs/FAQs.astro';
|
||||
import FrameRenderer from '../../components/FrameRenderer/FrameRenderer.astro';
|
||||
import RelatedRoadmaps from '../../components/RelatedRoadmaps.astro';
|
||||
import RoadmapHeader from '../../components/RoadmapHeader.astro';
|
||||
import { FolderKanbanIcon } from 'lucide-react';
|
||||
import { EmptyProjects } from '../../components/Projects/EmptyProjects';
|
||||
import ShareIcons from '../../components/ShareIcons/ShareIcons.astro';
|
||||
import { UserProgressModal } from '../../components/UserProgress/UserProgressModal';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getProjectsByRoadmapId } from '../../lib/project';
|
||||
import {
|
||||
generateArticleSchema,
|
||||
generateFAQSchema,
|
||||
} from '../../lib/jsonld-schema';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { type RoadmapFrontmatter, getRoadmapIds } from '../../lib/roadmap';
|
||||
import RoadmapNote from '../../components/RoadmapNote.astro';
|
||||
import { RoadmapTitleQuestion } from '../../components/RoadmapTitleQuestion';
|
||||
import ResourceProgressStats from '../../components/ResourceProgressStats.astro';
|
||||
import AstroIcon from '../../components/AstroIcon.astro';
|
||||
import CourseStep from '../../components/courses/CourseStep.astro';
|
||||
import Milestone from '../../components/courses/Milestone.astro';
|
||||
|
||||
@@ -95,24 +78,42 @@ const seoDescription = `Seeking ${nounTitle.toLowerCase()} courses to enhance yo
|
||||
<div class='relative my-3 rounded-lg border bg-white px-12 py-8'>
|
||||
<span class='absolute inset-y-0 left-[26.3px] w-[1px] bg-black'></span>
|
||||
|
||||
<div class='mb-8 flex flex-col gap-4 text-sm text-gray-500 leading-normal'>
|
||||
<div
|
||||
class='mb-8 flex flex-col gap-4 text-sm leading-normal text-gray-500'
|
||||
>
|
||||
<p>
|
||||
Frontend development is a vast field with a lot of tools and
|
||||
technologies. We have the <a class="font-medium underline underline-offset-2 text-black" href="/frontend">frontend roadmap</a>
|
||||
which is filled with a lot of <span class="font-medium text-black">free and good</span> resources to help you learn. But sometimes it helps to have a minimalistic list of courses
|
||||
and project recommendations to help you get started.
|
||||
technologies. We have the <a
|
||||
class='font-medium text-black underline underline-offset-2'
|
||||
href='/frontend'>frontend roadmap</a
|
||||
>
|
||||
which is filled with a lot of <span class='font-medium text-black'
|
||||
>free and good</span
|
||||
> resources to help you learn. But sometimes it helps to have a minimalistic
|
||||
list of courses and project recommendations to help you get started.
|
||||
</p>
|
||||
|
||||
<p class="bg-yellow-100 text-yellow-900 rounded-md p-2">
|
||||
Below are some of the best courses (paid) and projects to help you learn frontend development. These are handpicked and are a great way to get started.
|
||||
<p class='rounded-md bg-yellow-100 p-2 text-yellow-900'>
|
||||
Below are some of the best courses (paid) and projects to help you
|
||||
learn frontend development. These are handpicked and are a great way
|
||||
to get started.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Please note that these are paid courses curated from external platforms. We earn a small commission if you purchase the course using the links below. This helps us maintain the website and keep it free for everyone.
|
||||
Please note that these are paid courses curated from external
|
||||
platforms. We earn a small commission if you purchase the course
|
||||
using the links below. This helps us maintain the website and keep
|
||||
it free for everyone.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you are looking for free resources, you can check out the <a class="font-medium underline underline-offset-2 text-black" href="/frontend">frontend roadmap</a>. Also, we have a <a class="font-medium underline underline-offset-2 text-black" href="/frontend/projects">list of projects</a> that you can work on to enhance your skills.
|
||||
If you are looking for free resources, you can check out the <a
|
||||
class='font-medium text-black underline underline-offset-2'
|
||||
href='/frontend'>frontend roadmap</a
|
||||
>. Also, we have a <a
|
||||
class='font-medium text-black underline underline-offset-2'
|
||||
href='/frontend/projects'>list of projects</a
|
||||
> that you can work on to enhance your skills.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
@@ -4,7 +4,7 @@ import FAQs, { type FAQType } from '../../components/FAQs/FAQs.astro';
|
||||
import FrameRenderer from '../../components/FrameRenderer/FrameRenderer.astro';
|
||||
import RelatedRoadmaps from '../../components/RelatedRoadmaps.astro';
|
||||
import RoadmapHeader from '../../components/RoadmapHeader.astro';
|
||||
import ShareIcons from '../../components/ShareIcons/ShareIcons.astro';
|
||||
import { ShareIcons } from '../../components/ShareIcons/ShareIcons';
|
||||
import { TopicDetail } from '../../components/TopicDetail/TopicDetail';
|
||||
import { UserProgressModal } from '../../components/UserProgress/UserProgressModal';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
@@ -134,8 +134,11 @@ const projects = await getProjectsByRoadmapId(roadmapId);
|
||||
|
||||
<div class='container relative !max-w-[1000px]'>
|
||||
<ShareIcons
|
||||
resourceId={roadmapId}
|
||||
resourceType='roadmap'
|
||||
description={roadmapData.briefDescription}
|
||||
pageUrl={`https://roadmap.sh/${roadmapId}`}
|
||||
client:load
|
||||
/>
|
||||
|
||||
{
|
||||
|
@@ -2,7 +2,7 @@
|
||||
import BestPracticeHeader from '../../../components/BestPracticeHeader.astro';
|
||||
import FrameRenderer from '../../../components/FrameRenderer/FrameRenderer.astro';
|
||||
import MarkdownFile from '../../../components/MarkdownFile.astro';
|
||||
import ShareIcons from '../../../components/ShareIcons/ShareIcons.astro';
|
||||
import { ShareIcons } from '../../../components/ShareIcons/ShareIcons';
|
||||
import { TopicDetail } from '../../../components/TopicDetail/TopicDetail';
|
||||
import UpcomingForm from '../../../components/UpcomingForm.astro';
|
||||
import BaseLayout from '../../../layouts/BaseLayout.astro';
|
||||
@@ -94,8 +94,11 @@ const ogImageUrl = getOpenGraphImageUrl({
|
||||
!bestPracticeData.isUpcoming && bestPracticeData.jsonUrl && (
|
||||
<div class='container relative !max-w-[1000px]'>
|
||||
<ShareIcons
|
||||
resourceId={bestPracticeId}
|
||||
resourceType='best-practice'
|
||||
description={bestPracticeData.briefDescription}
|
||||
pageUrl={`https://roadmap.sh/best-practices/${bestPracticeId}`}
|
||||
client:load
|
||||
/>
|
||||
|
||||
<TopicDetail
|
||||
|
Reference in New Issue
Block a user