Merge branch 'master' into feat/ai-rdm-slug

feat/ai-rdm-slug
Arik Chakma 7 months ago
commit e4c0bad2c0
  1. 2
      src/components/FeaturedGuides.astro
  2. 2
      src/components/FeaturedVideos.astro
  3. 4
      src/components/GenerateRoadmap/AITermSuggestionInput.tsx
  4. 26
      src/components/Navigation/AccountDropdownList.tsx
  5. 24
      src/components/PageSponsor.tsx
  6. 25
      src/components/UpdateProfile/ProfileUsername.tsx
  7. 4
      src/components/UpdateProfile/UpdatePublicProfileForm.tsx
  8. 2
      src/data/question-groups/react/react.md
  9. 4
      src/data/roadmaps/devops/content/101-operating-systems/linux/100-ubuntu.md
  10. 4
      src/data/roadmaps/docker/content/104-data-persistence/100-ephemeral-container-fs.md
  11. 16
      src/lib/jwt.ts
  12. 8
      src/pages/u/[username].astro
  13. 2
      src/pages/v1-stats.json.ts

@ -11,7 +11,7 @@ const { heading, guides } = Astro.props;
--- ---
<div class='container'> <div class='container'>
<h1 class='text-2xl sm:text-3xl font-bold block'>{heading}</h1> <h2 class='text-2xl sm:text-3xl font-bold block'>{heading}</h2>
<div class='mt-3 sm:my-5'> <div class='mt-3 sm:my-5'>
{guides.map((guide) => <GuideListItem guide={guide} />)} {guides.map((guide) => <GuideListItem guide={guide} />)}

@ -11,7 +11,7 @@ const { heading, videos } = Astro.props;
--- ---
<div class='container'> <div class='container'>
<h1 class='text-2xl sm:text-3xl font-bold block'>{heading}</h1> <h2 class='text-2xl sm:text-3xl font-bold block'>{heading}</h2>
<div class='mt-3 sm:my-5'> <div class='mt-3 sm:my-5'>
{videos.map((video) => <VideoListItem video={video} />)} {videos.map((video) => <VideoListItem video={video} />)}

@ -69,6 +69,10 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) {
return []; return [];
} }
if (trimmedValue.length < 3) {
return [];
}
if (termCache.has(trimmedValue)) { if (termCache.has(trimmedValue)) {
const cachedData = termCache.get(trimmedValue); const cachedData = termCache.get(trimmedValue);
return cachedData || []; return cachedData || [];

@ -1,4 +1,12 @@
import { ChevronRight, LogOut, Map, Plus, User2, Users2 } from 'lucide-react'; import {
ChevronRight,
LogOut,
Map,
Plus,
SquareUserRound,
User2,
Users2,
} from 'lucide-react';
import { logout } from './navigation'; import { logout } from './navigation';
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx'; import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx';
import { useState } from 'react'; import { useState } from 'react';
@ -23,6 +31,20 @@ export function AccountDropdownList(props: AccountDropdownListProps) {
Account Account
</a> </a>
</li> </li>
<li className="px-1">
<a
href="/account/update-profile"
className="group flex items-center justify-between gap-2 rounded py-2 pl-3 pr-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
>
<span className="flex items-center gap-2">
<SquareUserRound className="h-4 w-4 stroke-[2.5px] text-slate-400 group-hover:text-white" />
My Profile
</span>
<span className="rounded-sm bg-yellow-300 px-1 text-xs uppercase tracking-wide text-black">
New
</span>
</a>
</li>
<li className="px-1"> <li className="px-1">
<a <a
href="/account/friends" href="/account/friends"
@ -66,7 +88,7 @@ export function AccountDropdownList(props: AccountDropdownListProps) {
</li> </li>
<li className="px-1"> <li className="px-1">
<button <button
className="group flex gap-2 items-center w-full rounded py-2 pl-3 pr-2 text-left text-sm font-medium text-slate-100 hover:bg-slate-700" className="group flex w-full items-center gap-2 rounded py-2 pl-3 pr-2 text-left text-sm font-medium text-slate-100 hover:bg-slate-700"
type="button" type="button"
onClick={logout} onClick={logout}
> >

@ -1,8 +1,9 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { httpGet } from '../lib/http'; import { httpGet, httpPatch, httpPost } from '../lib/http';
import { sponsorHidden } from '../stores/page'; import { sponsorHidden } from '../stores/page';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import { setViewSponsorCookie } from '../lib/jwt';
export type PageSponsorType = { export type PageSponsorType = {
company: string; company: string;
@ -15,6 +16,7 @@ export type PageSponsorType = {
}; };
type V1GetSponsorResponse = { type V1GetSponsorResponse = {
id?: string;
href?: string; href?: string;
sponsor?: PageSponsorType; sponsor?: PageSponsorType;
}; };
@ -26,6 +28,8 @@ type PageSponsorProps = {
export function PageSponsor(props: PageSponsorProps) { export function PageSponsor(props: PageSponsorProps) {
const { gaPageIdentifier } = props; const { gaPageIdentifier } = props;
const $isSponsorHidden = useStore(sponsorHidden); const $isSponsorHidden = useStore(sponsorHidden);
const [sponsorId, setSponsorId] = useState<string | null>(null);
const [sponsor, setSponsor] = useState<PageSponsorType>(); const [sponsor, setSponsor] = useState<PageSponsorType>();
const loadSponsor = async () => { const loadSponsor = async () => {
@ -59,6 +63,7 @@ export function PageSponsor(props: PageSponsorProps) {
} }
setSponsor(response.sponsor); setSponsor(response.sponsor);
setSponsorId(response?.id || null);
window.fireEvent({ window.fireEvent({
category: 'SponsorImpression', category: 'SponsorImpression',
@ -69,6 +74,20 @@ export function PageSponsor(props: PageSponsorProps) {
}); });
}; };
const clickSponsor = async (sponsorId: string) => {
const { response, error } = await httpPatch<{ status: 'ok' }>(
`${import.meta.env.PUBLIC_API_URL}/v1-view-sponsor/${sponsorId}`,
{},
);
if (error || !response) {
console.error(error);
return;
}
setViewSponsorCookie(sponsorId);
};
useEffect(() => { useEffect(() => {
window.setTimeout(loadSponsor); window.setTimeout(loadSponsor);
}, []); }, []);
@ -85,12 +104,13 @@ export function PageSponsor(props: PageSponsorProps) {
target="_blank" target="_blank"
rel="noopener sponsored nofollow" rel="noopener sponsored nofollow"
className="fixed bottom-[15px] right-[15px] z-50 flex max-w-[350px] bg-white shadow-lg outline-0 outline-transparent" className="fixed bottom-[15px] right-[15px] z-50 flex max-w-[350px] bg-white shadow-lg outline-0 outline-transparent"
onClick={() => { onClick={async () => {
window.fireEvent({ window.fireEvent({
category: 'SponsorClick', category: 'SponsorClick',
action: `${company} Redirect`, action: `${company} Redirect`,
label: gaLabel || `${gaPageIdentifier} / ${company} Link`, label: gaLabel || `${gaPageIdentifier} / ${company} Link`,
}); });
await clickSponsor(sponsorId || '');
}} }}
> >
<span <span

@ -1,8 +1,9 @@
import { useState } from 'react'; import { useEffect, useState } from 'react';
import type { AllowedProfileVisibility } from '../../api/user'; import type { AllowedProfileVisibility } from '../../api/user';
import { httpGet, httpPost } from '../../lib/http'; import { httpGet, httpPost } from '../../lib/http';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import { CheckIcon, Loader2, X, XCircle } from 'lucide-react'; import { CheckIcon, Loader2, X, XCircle } from 'lucide-react';
import { useDebounceValue } from '../../hooks/use-debounce.ts';
type ProfileUsernameProps = { type ProfileUsernameProps = {
username: string; username: string;
@ -17,6 +18,11 @@ export function ProfileUsername(props: ProfileUsernameProps) {
const toast = useToast(); const toast = useToast();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isUnique, setIsUnique] = useState<boolean | null>(null); const [isUnique, setIsUnique] = useState<boolean | null>(null);
const debouncedUsername = useDebounceValue(username, 500);
useEffect(() => {
checkIsUnique(debouncedUsername).then();
}, [debouncedUsername]);
const checkIsUnique = async (username: string) => { const checkIsUnique = async (username: string) => {
if (isLoading || username.length < 3) { if (isLoading || username.length < 3) {
@ -66,7 +72,22 @@ export function ProfileUsername(props: ProfileUsernameProps) {
spellCheck={false} spellCheck={false}
value={username} value={username}
title="Username must be at least 3 characters long and can only contain letters, numbers, and underscores" title="Username must be at least 3 characters long and can only contain letters, numbers, and underscores"
onChange={(e) => setUsername((e.target as HTMLInputElement).value)} onKeyDown={(e) => {
// only allow letters, numbers
const keyCode = e.key;
const validKey = /^[a-zA-Z0-9]*$/.test(keyCode);
if (
!validKey &&
!['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight'].includes(
keyCode,
)
) {
e.preventDefault();
}
}}
onChange={(e) => {
setUsername((e.target as HTMLInputElement).value.toLowerCase());
}}
onBlur={(e) => checkIsUnique((e.target as HTMLInputElement).value)} onBlur={(e) => checkIsUnique((e.target as HTMLInputElement).value)}
required={profileVisibility === 'public'} required={profileVisibility === 'public'}
/> />

@ -27,7 +27,7 @@ type GetProfileSettingsResponse = Pick<
export function UpdatePublicProfileForm() { export function UpdatePublicProfileForm() {
const [profileVisibility, setProfileVisibility] = const [profileVisibility, setProfileVisibility] =
useState<AllowedProfileVisibility>('private'); useState<AllowedProfileVisibility>('public');
const toast = useToast(); const toast = useToast();
@ -117,7 +117,7 @@ export function UpdatePublicProfileForm() {
setTwitter(links?.twitter || ''); setTwitter(links?.twitter || '');
setLinkedin(links?.linkedin || ''); setLinkedin(links?.linkedin || '');
setWebsite(links?.website || ''); setWebsite(links?.website || '');
setProfileVisibility(defaultProfileVisibility || 'private'); setProfileVisibility(defaultProfileVisibility || 'public');
setHeadline(publicConfig?.headline || ''); setHeadline(publicConfig?.headline || '');
setRoadmapVisibility(publicConfig?.roadmapVisibility || 'none'); setRoadmapVisibility(publicConfig?.roadmapVisibility || 'none');
setCustomRoadmapVisibility(publicConfig?.customRoadmapVisibility || 'none'); setCustomRoadmapVisibility(publicConfig?.customRoadmapVisibility || 'none');

@ -210,7 +210,7 @@ questions:
- 'Intermediate' - 'Intermediate'
- question: What are Server Components in React? - question: What are Server Components in React?
answer: | answer: |
Server Components in allow developers to write components that render on the server instead of the client. Unlike traditional components, Server Components do not have a client-side runtime, meaning they result in a smaller bundle size and faster loads. They can seamlessly integrate with client components and can fetch data directly from the backend without the need for an API layer. This enables developers to build rich, interactive apps with less client-side code, improving performance and developer experience. Server Components allow developers to write components that render on the server instead of the client. Unlike traditional components, Server Components do not have a client-side runtime, meaning they result in a smaller bundle size and faster loads. They can seamlessly integrate with client components and can fetch data directly from the backend without the need for an API layer. This enables developers to build rich, interactive apps with less client-side code, improving performance and developer experience.
topics: topics:
- 'SSR' - 'SSR'
- 'Intermediate' - 'Intermediate'

@ -11,6 +11,6 @@ Visit the following resources to learn more:
- [Learn the ways of Linux-fu, for free](https://linuxjourney.com/) - [Learn the ways of Linux-fu, for free](https://linuxjourney.com/)
- [Linux Operating System - Crash Course for Beginners](https://www.youtube.com/watch?v=ROjZy1WbCIA) - [Linux Operating System - Crash Course for Beginners](https://www.youtube.com/watch?v=ROjZy1WbCIA)
- [The Linux Command Line by William Shotts](https://linuxcommand.org/tlcl.php) - [The Linux Command Line by William Shotts](https://linuxcommand.org/tlcl.php)
- [r/linuxupskillchallenge](https://www.reddit.com/r/linuxupskillchallenge/) - [Linux Upskill Challenge](https://linuxupskillchallenge.org/)
- [Introduction to Linux - Full Course for Beginners](https://www.youtube.com/watch?v=sWbUDq4S6Y8&pp=ygUTVWJ1bnR1IGNyYXNoIGNvdXJzZQ%3D%3D) - [Introduction to Linux - Full Course for Beginners](https://www.youtube.com/watch?v=sWbUDq4S6Y8&pp=ygUTVWJ1bnR1IGNyYXNoIGNvdXJzZQ%3D%3D)
- [Linux Fundamentals](https://academy.hackthebox.com/course/preview/linux-fundamentals) - [Linux Fundamentals](https://academy.hackthebox.com/course/preview/linux-fundamentals)

@ -6,7 +6,7 @@ This temporary or short-lived storage is called the "ephemeral container file sy
### Ephemeral FS and Data Persistence ### Ephemeral FS and Data Persistence
As any data stored within the container's ephemeral FS is lost when the container is stopped or removed, it poses a challenge to data persistence in applications. This is especially problematic for applications like databases, which require data to be persisted across multiple container life cycles. As any data stored within the container's ephemeral FS is lost when the container is stopped and removed, it poses a challenge to data persistence in applications. This is especially problematic for applications like databases, which require data to be persisted across multiple container life cycles.
To overcome these challenges, Docker provides several methods for data persistence, such as: To overcome these challenges, Docker provides several methods for data persistence, such as:
@ -14,4 +14,4 @@ To overcome these challenges, Docker provides several methods for data persisten
- **Bind mounts**: Mapping a host machine's directory or file into a container, effectively sharing host's storage with the container. - **Bind mounts**: Mapping a host machine's directory or file into a container, effectively sharing host's storage with the container.
- **tmpfs mounts**: In-memory storage, useful for cases where just the persistence of data within the life-cycle of the container is required. - **tmpfs mounts**: In-memory storage, useful for cases where just the persistence of data within the life-cycle of the container is required.
By implementing these strategies, Docker ensures that application data can be preserved beyond the life-cycle of a single container, making it possible to work with stateful applications. By implementing these strategies, Docker ensures that application data can be preserved beyond the life-cycle of a single container, making it possible to work with stateful applications.

@ -109,3 +109,19 @@ export function removeAIReferralCode() {
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
}); });
} }
export function setViewSponsorCookie(sponsorId: string) {
const key = `vsc-${sponsorId}`;
const alreadyExist = Cookies.get(key);
if (alreadyExist) {
return;
}
Cookies.set(key, '1', {
path: '/',
expires: 1,
sameSite: 'lax',
secure: true,
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
});
}

@ -5,6 +5,7 @@ import AccountLayout from '../../layouts/AccountLayout.astro';
import { UserPublicProfilePage } from '../../components/UserPublicProfile/UserPublicProfilePage'; import { UserPublicProfilePage } from '../../components/UserPublicProfile/UserPublicProfilePage';
import OpenSourceBanner from '../../components/OpenSourceBanner.astro'; import OpenSourceBanner from '../../components/OpenSourceBanner.astro';
import Footer from '../../components/Footer.astro'; import Footer from '../../components/Footer.astro';
import BaseLayout from "../../layouts/BaseLayout.astro";
export const prerender = false; export const prerender = false;
@ -27,7 +28,7 @@ if (error || !userDetails) {
} }
--- ---
<AccountLayout title={userDetails?.name} errorMessage={errorMessage}> <BaseLayout title={`${userDetails?.name} - Skill Profile at roadmap.sh`}>
{!errorMessage && <UserPublicProfilePage {...userDetails} client:load />} {!errorMessage && <UserPublicProfilePage {...userDetails} client:load />}
{ {
errorMessage && ( errorMessage && (
@ -55,7 +56,4 @@ if (error || !userDetails) {
</div> </div>
) )
} }
</BaseLayout>
<OpenSourceBanner />
<Footer />
</AccountLayout>

@ -1,6 +1,6 @@
import { execSync } from 'child_process'; import { execSync } from 'child_process';
export const prerender = false; export const prerender = true;
export async function GET() { export async function GET() {
const commitHash = execSync('git rev-parse HEAD').toString().trim(); const commitHash = execSync('git rev-parse HEAD').toString().trim();

Loading…
Cancel
Save