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. 2
      src/data/roadmaps/devops/content/101-operating-systems/linux/100-ubuntu.md
  10. 2
      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'>
<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'>
{guides.map((guide) => <GuideListItem guide={guide} />)}

@ -11,7 +11,7 @@ const { heading, videos } = Astro.props;
---
<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'>
{videos.map((video) => <VideoListItem video={video} />)}

@ -69,6 +69,10 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) {
return [];
}
if (trimmedValue.length < 3) {
return [];
}
if (termCache.has(trimmedValue)) {
const cachedData = termCache.get(trimmedValue);
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 { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx';
import { useState } from 'react';
@ -23,6 +31,20 @@ export function AccountDropdownList(props: AccountDropdownListProps) {
Account
</a>
</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">
<a
href="/account/friends"
@ -66,7 +88,7 @@ export function AccountDropdownList(props: AccountDropdownListProps) {
</li>
<li className="px-1">
<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"
onClick={logout}
>

@ -1,8 +1,9 @@
import { useEffect, useState } from 'react';
import { httpGet } from '../lib/http';
import { httpGet, httpPatch, httpPost } from '../lib/http';
import { sponsorHidden } from '../stores/page';
import { useStore } from '@nanostores/react';
import { X } from 'lucide-react';
import { setViewSponsorCookie } from '../lib/jwt';
export type PageSponsorType = {
company: string;
@ -15,6 +16,7 @@ export type PageSponsorType = {
};
type V1GetSponsorResponse = {
id?: string;
href?: string;
sponsor?: PageSponsorType;
};
@ -26,6 +28,8 @@ type PageSponsorProps = {
export function PageSponsor(props: PageSponsorProps) {
const { gaPageIdentifier } = props;
const $isSponsorHidden = useStore(sponsorHidden);
const [sponsorId, setSponsorId] = useState<string | null>(null);
const [sponsor, setSponsor] = useState<PageSponsorType>();
const loadSponsor = async () => {
@ -59,6 +63,7 @@ export function PageSponsor(props: PageSponsorProps) {
}
setSponsor(response.sponsor);
setSponsorId(response?.id || null);
window.fireEvent({
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(() => {
window.setTimeout(loadSponsor);
}, []);
@ -85,12 +104,13 @@ export function PageSponsor(props: PageSponsorProps) {
target="_blank"
rel="noopener sponsored nofollow"
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({
category: 'SponsorClick',
action: `${company} Redirect`,
label: gaLabel || `${gaPageIdentifier} / ${company} Link`,
});
await clickSponsor(sponsorId || '');
}}
>
<span

@ -1,8 +1,9 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import type { AllowedProfileVisibility } from '../../api/user';
import { httpGet, httpPost } from '../../lib/http';
import { useToast } from '../../hooks/use-toast';
import { CheckIcon, Loader2, X, XCircle } from 'lucide-react';
import { useDebounceValue } from '../../hooks/use-debounce.ts';
type ProfileUsernameProps = {
username: string;
@ -17,6 +18,11 @@ export function ProfileUsername(props: ProfileUsernameProps) {
const toast = useToast();
const [isLoading, setIsLoading] = useState(false);
const [isUnique, setIsUnique] = useState<boolean | null>(null);
const debouncedUsername = useDebounceValue(username, 500);
useEffect(() => {
checkIsUnique(debouncedUsername).then();
}, [debouncedUsername]);
const checkIsUnique = async (username: string) => {
if (isLoading || username.length < 3) {
@ -66,7 +72,22 @@ export function ProfileUsername(props: ProfileUsernameProps) {
spellCheck={false}
value={username}
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)}
required={profileVisibility === 'public'}
/>

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

@ -210,7 +210,7 @@ questions:
- 'Intermediate'
- question: What are Server Components in React?
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:
- 'SSR'
- 'Intermediate'

@ -11,6 +11,6 @@ Visit the following resources to learn more:
- [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)
- [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)
- [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
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:

@ -109,3 +109,19 @@ export function removeAIReferralCode() {
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 OpenSourceBanner from '../../components/OpenSourceBanner.astro';
import Footer from '../../components/Footer.astro';
import BaseLayout from "../../layouts/BaseLayout.astro";
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 && (
@ -55,7 +56,4 @@ if (error || !userDetails) {
</div>
)
}
<OpenSourceBanner />
<Footer />
</AccountLayout>
</BaseLayout>

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

Loading…
Cancel
Save