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>
feat/sub-changelog^2
Arik Chakma 2 weeks ago committed by GitHub
parent 0bef28fa20
commit 1ffa292c98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 23
      src/components/AuthenticationFlow/EmailLoginForm.tsx
  2. 31
      src/components/AuthenticationFlow/GitHubButton.tsx
  3. 36
      src/components/AuthenticationFlow/GoogleButton.tsx
  4. 36
      src/components/AuthenticationFlow/LinkedInButton.tsx
  5. 11
      src/components/AuthenticationFlow/TriggerVerifyAccount.tsx
  6. 20
      src/components/ReactIcons/HackerNewsIcon.tsx
  7. 20
      src/components/ReactIcons/RedditIcon.tsx
  8. 2
      src/components/ReactIcons/TwitterIcon.tsx
  9. 34
      src/components/ShareIcons/ShareIcons.astro
  10. 105
      src/components/ShareIcons/ShareIcons.tsx
  11. 32
      src/components/ShareIcons/sharer.js
  12. 1
      src/lib/jwt.ts
  13. 51
      src/pages/[roadmapId]/courses.astro
  14. 5
      src/pages/[roadmapId]/index.astro
  15. 5
      src/pages/best-practices/[bestPracticeId]/index.astro

@ -2,7 +2,7 @@ import Cookies from 'js-cookie';
import type { FormEvent } from 'react'; import type { FormEvent } from 'react';
import { useId, useState } from 'react'; import { useId, useState } from 'react';
import { httpPost } from '../../lib/http'; import { httpPost } from '../../lib/http';
import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt'; import { FIRST_LOGIN_PARAM, setAuthToken } from '../../lib/jwt';
type EmailLoginFormProps = { type EmailLoginFormProps = {
isDisabled?: boolean; isDisabled?: boolean;
@ -24,19 +24,24 @@ export function EmailLoginForm(props: EmailLoginFormProps) {
setIsDisabled?.(true); setIsDisabled?.(true);
setError(''); setError('');
const { response, error } = await httpPost<{ token: string }>( const { response, error } = await httpPost<{
`${import.meta.env.PUBLIC_API_URL}/v1-login`, token: string;
{ isNewUser: boolean;
email, }>(`${import.meta.env.PUBLIC_API_URL}/v1-login`, {
password, email,
}, password,
); });
// Log the user in and reload the page // Log the user in and reload the page
if (response?.token) { if (response?.token) {
setAuthToken(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; return;
} }

@ -1,8 +1,12 @@
import { useEffect, useState } from 'react'; 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 { cn } from '../../../editor/utils/classname.ts';
import { httpGet } from '../../lib/http'; 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 { Spinner } from '../ReactIcons/Spinner.tsx';
import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx'; import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx';
import { triggerUtmRegistration } from '../../lib/browser.ts'; import { triggerUtmRegistration } from '../../lib/browser.ts';
@ -34,7 +38,7 @@ export function GitHubButton(props: GitHubButtonProps) {
setIsLoading(true); setIsLoading(true);
setIsDisabled?.(true); setIsDisabled?.(true);
httpGet<{ token: string }>( httpGet<{ token: string; isNewUser: boolean }>(
`${import.meta.env.PUBLIC_API_URL}/v1-github-callback${ `${import.meta.env.PUBLIC_API_URL}/v1-github-callback${
window.location.search window.location.search
}`, }`,
@ -51,7 +55,7 @@ export function GitHubButton(props: GitHubButtonProps) {
triggerUtmRegistration(); triggerUtmRegistration();
let redirectUrl = '/'; let redirectUrl = new URL('/', window.location.origin);
const gitHubRedirectAt = localStorage.getItem(GITHUB_REDIRECT_AT); const gitHubRedirectAt = localStorage.getItem(GITHUB_REDIRECT_AT);
const lastPageBeforeGithub = localStorage.getItem(GITHUB_LAST_PAGE); const lastPageBeforeGithub = localStorage.getItem(GITHUB_LAST_PAGE);
@ -63,31 +67,36 @@ export function GitHubButton(props: GitHubButtonProps) {
const timeSinceRedirect = now - socialRedirectAtTime; const timeSinceRedirect = now - socialRedirectAtTime;
if (timeSinceRedirect < 30 * 1000) { if (timeSinceRedirect < 30 * 1000) {
redirectUrl = lastPageBeforeGithub; redirectUrl = new URL(lastPageBeforeGithub, window.location.origin);
} }
} }
const authRedirectUrl = localStorage.getItem('authRedirect'); const authRedirectUrl = localStorage.getItem('authRedirect');
if (authRedirectUrl) { if (authRedirectUrl) {
localStorage.removeItem('authRedirect'); localStorage.removeItem('authRedirect');
redirectUrl = authRedirectUrl; redirectUrl = new URL(authRedirectUrl, window.location.origin);
} }
localStorage.removeItem(GITHUB_REDIRECT_AT); localStorage.removeItem(GITHUB_REDIRECT_AT);
localStorage.removeItem(GITHUB_LAST_PAGE); localStorage.removeItem(GITHUB_LAST_PAGE);
setAuthToken(response.token); setAuthToken(response.token);
if (response?.isNewUser) {
redirectUrl.searchParams.set(FIRST_LOGIN_PARAM, '1');
}
const shouldTriggerPurchase = const shouldTriggerPurchase =
localStorage.getItem(CHECKOUT_AFTER_LOGIN_KEY) !== '0'; 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(CHECKOUT_AFTER_LOGIN_KEY);
} }
window.location.href = redirectUrl; window.location.href = redirectUrl.toString();
}) })
.catch((err) => { .catch((err) => {
setError('Something went wrong. Please try again later.'); setError('Something went wrong. Please try again later.');

@ -1,7 +1,12 @@
import { useEffect, useState } from 'react'; 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 { 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 { GoogleIcon } from '../ReactIcons/GoogleIcon.tsx';
import { Spinner } from '../ReactIcons/Spinner.tsx'; import { Spinner } from '../ReactIcons/Spinner.tsx';
import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx'; import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx';
@ -9,6 +14,7 @@ import {
getStoredUtmParams, getStoredUtmParams,
triggerUtmRegistration, triggerUtmRegistration,
} from '../../lib/browser.ts'; } from '../../lib/browser.ts';
import { cn } from '../../lib/classname.ts';
type GoogleButtonProps = { type GoogleButtonProps = {
isDisabled?: boolean; isDisabled?: boolean;
@ -37,14 +43,12 @@ export function GoogleButton(props: GoogleButtonProps) {
setIsLoading(true); setIsLoading(true);
setIsDisabled?.(true); setIsDisabled?.(true);
httpGet<{ token: string }>( httpGet<{ token: string; isNewUser: boolean }>(
`${import.meta.env.PUBLIC_API_URL}/v1-google-callback${ `${import.meta.env.PUBLIC_API_URL}/v1-google-callback${
window.location.search window.location.search
}`, }`,
) )
.then(({ response, error }) => { .then(({ response, error }) => {
const utmParams = getStoredUtmParams();
if (!response?.token) { if (!response?.token) {
setError(error?.message || 'Something went wrong.'); setError(error?.message || 'Something went wrong.');
setIsLoading(false); setIsLoading(false);
@ -55,7 +59,7 @@ export function GoogleButton(props: GoogleButtonProps) {
triggerUtmRegistration(); triggerUtmRegistration();
let redirectUrl = '/'; let redirectUrl = new URL('/', window.location.origin);
const googleRedirectAt = localStorage.getItem(GOOGLE_REDIRECT_AT); const googleRedirectAt = localStorage.getItem(GOOGLE_REDIRECT_AT);
const lastPageBeforeGoogle = localStorage.getItem(GOOGLE_LAST_PAGE); const lastPageBeforeGoogle = localStorage.getItem(GOOGLE_LAST_PAGE);
@ -67,22 +71,27 @@ export function GoogleButton(props: GoogleButtonProps) {
const timeSinceRedirect = now - socialRedirectAtTime; const timeSinceRedirect = now - socialRedirectAtTime;
if (timeSinceRedirect < 30 * 1000) { if (timeSinceRedirect < 30 * 1000) {
redirectUrl = lastPageBeforeGoogle; redirectUrl = new URL(lastPageBeforeGoogle, window.location.origin);
} }
} }
const authRedirectUrl = localStorage.getItem('authRedirect'); const authRedirectUrl = localStorage.getItem('authRedirect');
if (authRedirectUrl) { if (authRedirectUrl) {
localStorage.removeItem('authRedirect'); localStorage.removeItem('authRedirect');
redirectUrl = authRedirectUrl; redirectUrl = new URL(authRedirectUrl, window.location.origin);
}
if (response?.isNewUser) {
redirectUrl.searchParams.set(FIRST_LOGIN_PARAM, '1');
} }
const shouldTriggerPurchase = const shouldTriggerPurchase =
localStorage.getItem(CHECKOUT_AFTER_LOGIN_KEY) !== '0'; localStorage.getItem(CHECKOUT_AFTER_LOGIN_KEY) !== '0';
if (redirectUrl.includes('/courses/sql') && shouldTriggerPurchase) { if (
const tempUrl = new URL(redirectUrl, window.location.origin); redirectUrl.pathname.includes('/courses/sql') &&
tempUrl.searchParams.set(COURSE_PURCHASE_PARAM, '1'); shouldTriggerPurchase
redirectUrl = tempUrl.toString(); ) {
redirectUrl.searchParams.set(COURSE_PURCHASE_PARAM, '1');
localStorage.removeItem(CHECKOUT_AFTER_LOGIN_KEY); localStorage.removeItem(CHECKOUT_AFTER_LOGIN_KEY);
} }
@ -90,7 +99,8 @@ export function GoogleButton(props: GoogleButtonProps) {
localStorage.removeItem(GOOGLE_REDIRECT_AT); localStorage.removeItem(GOOGLE_REDIRECT_AT);
localStorage.removeItem(GOOGLE_LAST_PAGE); localStorage.removeItem(GOOGLE_LAST_PAGE);
setAuthToken(response.token); setAuthToken(response.token);
window.location.href = redirectUrl;
window.location.href = redirectUrl.toString();
}) })
.catch((err) => { .catch((err) => {
setError('Something went wrong. Please try again later.'); setError('Something went wrong. Please try again later.');

@ -1,7 +1,13 @@
import { useEffect, useState } from 'react'; 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 { cn } from '../../lib/classname.ts';
import { httpGet } from '../../lib/http'; import { httpGet } from '../../lib/http';
import { COURSE_PURCHASE_PARAM, setAuthToken } from '../../lib/jwt';
import { LinkedInIcon } from '../ReactIcons/LinkedInIcon.tsx'; import { LinkedInIcon } from '../ReactIcons/LinkedInIcon.tsx';
import { Spinner } from '../ReactIcons/Spinner.tsx'; import { Spinner } from '../ReactIcons/Spinner.tsx';
import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx'; import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx';
@ -34,7 +40,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
setIsLoading(true); setIsLoading(true);
setIsDisabled?.(true); setIsDisabled?.(true);
httpGet<{ token: string }>( httpGet<{ token: string; isNewUser: boolean }>(
`${import.meta.env.PUBLIC_API_URL}/v1-linkedin-callback${ `${import.meta.env.PUBLIC_API_URL}/v1-linkedin-callback${
window.location.search window.location.search
}`, }`,
@ -50,7 +56,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
triggerUtmRegistration(); triggerUtmRegistration();
let redirectUrl = '/'; let redirectUrl = new URL('/', window.location.origin);
const linkedInRedirectAt = localStorage.getItem(LINKEDIN_REDIRECT_AT); const linkedInRedirectAt = localStorage.getItem(LINKEDIN_REDIRECT_AT);
const lastPageBeforeLinkedIn = localStorage.getItem(LINKEDIN_LAST_PAGE); const lastPageBeforeLinkedIn = localStorage.getItem(LINKEDIN_LAST_PAGE);
@ -62,30 +68,38 @@ export function LinkedInButton(props: LinkedInButtonProps) {
const timeSinceRedirect = now - socialRedirectAtTime; const timeSinceRedirect = now - socialRedirectAtTime;
if (timeSinceRedirect < 30 * 1000) { if (timeSinceRedirect < 30 * 1000) {
redirectUrl = lastPageBeforeLinkedIn; redirectUrl = new URL(
lastPageBeforeLinkedIn,
window.location.origin,
);
} }
} }
const authRedirectUrl = localStorage.getItem('authRedirect'); const authRedirectUrl = localStorage.getItem('authRedirect');
if (authRedirectUrl) { if (authRedirectUrl) {
localStorage.removeItem('authRedirect'); localStorage.removeItem('authRedirect');
redirectUrl = authRedirectUrl; redirectUrl = new URL(authRedirectUrl, window.location.origin);
}
if (response?.isNewUser) {
redirectUrl.searchParams.set(FIRST_LOGIN_PARAM, '1');
} }
const shouldTriggerPurchase = const shouldTriggerPurchase =
localStorage.getItem(CHECKOUT_AFTER_LOGIN_KEY) !== '0'; localStorage.getItem(CHECKOUT_AFTER_LOGIN_KEY) !== '0';
if (redirectUrl.includes('/courses/sql') && shouldTriggerPurchase) { if (
const tempUrl = new URL(redirectUrl, window.location.origin); redirectUrl.pathname.includes('/courses/sql') &&
tempUrl.searchParams.set(COURSE_PURCHASE_PARAM, '1'); shouldTriggerPurchase
redirectUrl = tempUrl.toString(); ) {
redirectUrl.searchParams.set(COURSE_PURCHASE_PARAM, '1');
localStorage.removeItem(CHECKOUT_AFTER_LOGIN_KEY); localStorage.removeItem(CHECKOUT_AFTER_LOGIN_KEY);
} }
localStorage.removeItem(LINKEDIN_REDIRECT_AT); localStorage.removeItem(LINKEDIN_REDIRECT_AT);
localStorage.removeItem(LINKEDIN_LAST_PAGE); localStorage.removeItem(LINKEDIN_LAST_PAGE);
setAuthToken(response.token); setAuthToken(response.token);
window.location.href = redirectUrl;
window.location.href = redirectUrl.toString();
}) })
.catch((err) => { .catch((err) => {
setError('Something went wrong. Please try again later.'); setError('Something went wrong. Please try again later.');

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { httpPost } from '../../lib/http'; 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 { Spinner } from '../ReactIcons/Spinner';
import { ErrorIcon2 } from '../ReactIcons/ErrorIcon2'; import { ErrorIcon2 } from '../ReactIcons/ErrorIcon2';
import { triggerUtmRegistration } from '../../lib/browser.ts'; import { triggerUtmRegistration } from '../../lib/browser.ts';
@ -13,7 +13,7 @@ export function TriggerVerifyAccount() {
const triggerVerify = (code: string) => { const triggerVerify = (code: string) => {
setIsLoading(true); setIsLoading(true);
httpPost<{ token: string }>( httpPost<{ token: string; isNewUser: boolean }>(
`${import.meta.env.PUBLIC_API_URL}/v1-verify-account`, `${import.meta.env.PUBLIC_API_URL}/v1-verify-account`,
{ {
code, code,
@ -30,7 +30,12 @@ export function TriggerVerifyAccount() {
triggerUtmRegistration(); triggerUtmRegistration();
setAuthToken(response.token); 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) => { .catch((err) => {
setIsLoading(false); setIsLoading(false);

@ -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>
);
}

@ -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} /> <rect width="23" height="23" rx="3" fill={boxColor} />
<path <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" 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> </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>

@ -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'; import type { AllowedOnboardingStatus } from '../api/user';
export const TOKEN_COOKIE_NAME = '__roadmapsh_jt__'; export const TOKEN_COOKIE_NAME = '__roadmapsh_jt__';
export const FIRST_LOGIN_PARAM = 'fl' as const;
export const COURSE_PURCHASE_PARAM = 't'; export const COURSE_PURCHASE_PARAM = 't';
export type TokenPayload = { 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 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 BaseLayout from '../../layouts/BaseLayout.astro';
import { getProjectsByRoadmapId } from '../../lib/project';
import {
generateArticleSchema,
generateFAQSchema,
} from '../../lib/jsonld-schema';
import { getOpenGraphImageUrl } from '../../lib/open-graph'; import { getOpenGraphImageUrl } from '../../lib/open-graph';
import { type RoadmapFrontmatter, getRoadmapIds } from '../../lib/roadmap'; 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 CourseStep from '../../components/courses/CourseStep.astro';
import Milestone from '../../components/courses/Milestone.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'> <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> <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> <p>
Frontend development is a vast field with a lot of tools and 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> technologies. We have the <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 class='font-medium text-black underline underline-offset-2'
and project recommendations to help you get started. 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>
<p class="bg-yellow-100 text-yellow-900 rounded-md p-2"> <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. 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>
<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>
<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> </p>
</div> </div>

@ -4,7 +4,7 @@ import FAQs, { type FAQType } from '../../components/FAQs/FAQs.astro';
import FrameRenderer from '../../components/FrameRenderer/FrameRenderer.astro'; import FrameRenderer from '../../components/FrameRenderer/FrameRenderer.astro';
import RelatedRoadmaps from '../../components/RelatedRoadmaps.astro'; import RelatedRoadmaps from '../../components/RelatedRoadmaps.astro';
import RoadmapHeader from '../../components/RoadmapHeader.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 { TopicDetail } from '../../components/TopicDetail/TopicDetail';
import { UserProgressModal } from '../../components/UserProgress/UserProgressModal'; import { UserProgressModal } from '../../components/UserProgress/UserProgressModal';
import BaseLayout from '../../layouts/BaseLayout.astro'; import BaseLayout from '../../layouts/BaseLayout.astro';
@ -134,8 +134,11 @@ const projects = await getProjectsByRoadmapId(roadmapId);
<div class='container relative !max-w-[1000px]'> <div class='container relative !max-w-[1000px]'>
<ShareIcons <ShareIcons
resourceId={roadmapId}
resourceType='roadmap'
description={roadmapData.briefDescription} description={roadmapData.briefDescription}
pageUrl={`https://roadmap.sh/${roadmapId}`} pageUrl={`https://roadmap.sh/${roadmapId}`}
client:load
/> />
{ {

@ -2,7 +2,7 @@
import BestPracticeHeader from '../../../components/BestPracticeHeader.astro'; import BestPracticeHeader from '../../../components/BestPracticeHeader.astro';
import FrameRenderer from '../../../components/FrameRenderer/FrameRenderer.astro'; import FrameRenderer from '../../../components/FrameRenderer/FrameRenderer.astro';
import MarkdownFile from '../../../components/MarkdownFile.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 { TopicDetail } from '../../../components/TopicDetail/TopicDetail';
import UpcomingForm from '../../../components/UpcomingForm.astro'; import UpcomingForm from '../../../components/UpcomingForm.astro';
import BaseLayout from '../../../layouts/BaseLayout.astro'; import BaseLayout from '../../../layouts/BaseLayout.astro';
@ -94,8 +94,11 @@ const ogImageUrl = getOpenGraphImageUrl({
!bestPracticeData.isUpcoming && bestPracticeData.jsonUrl && ( !bestPracticeData.isUpcoming && bestPracticeData.jsonUrl && (
<div class='container relative !max-w-[1000px]'> <div class='container relative !max-w-[1000px]'>
<ShareIcons <ShareIcons
resourceId={bestPracticeId}
resourceType='best-practice'
description={bestPracticeData.briefDescription} description={bestPracticeData.briefDescription}
pageUrl={`https://roadmap.sh/best-practices/${bestPracticeId}`} pageUrl={`https://roadmap.sh/best-practices/${bestPracticeId}`}
client:load
/> />
<TopicDetail <TopicDetail

Loading…
Cancel
Save