Compare commits

...

10 Commits

Author SHA1 Message Date
Arik Chakma
0264b88069 Merge branch 'master' into fix/activity 2024-05-08 04:16:18 +06:00
dsh
71d84faf73 None English video removed from node (#5610)
The youtube video linked for Horizontal Autoscaling was not English which goes against our contribution guidelines.
2024-05-07 20:17:10 +06:00
Arik Chakma
66e4793032 feat: team personal progress only (#5586)
* feat: team personal progress only

* fix: default false
2024-05-07 14:56:44 +01:00
Arik Chakma
32cbfd6699 feat: migrate road card endpoint (#5583) 2024-05-07 13:38:57 +01:00
Md. Jahidul Islam
043bf59c87 Add resource for Insertion Sort (#5591)
* Update 102-insertion-sort.md

Added insertion sort link from w3 school and also added advantages , disadvantages about insertion sort

* Reformat links

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2024-05-07 11:42:13 +01:00
Ed Lan
3e0d8a5b3d Updated featured image with tweaked dimensions (#5604) 2024-05-06 17:53:50 +01:00
Kamran Ahmed
2b20996134 Show proper error on open ai model 2024-05-05 19:03:01 +01:00
guangwu
20be28653f fix: following typo (#5308)
* fix: typo

* fix: gap

---------

Co-authored-by: Arik Chakma <arikchangma@gmail.com>
2024-05-04 09:10:54 +06:00
Arik Chakma
755e494224 fix: comma and gap 2024-05-03 23:32:44 +06:00
Arik Chakma
f6d4da48f9 fix: change topicIds to topicTitles 2024-05-03 04:57:59 +06:00
19 changed files with 288 additions and 225 deletions

View File

@@ -3,7 +3,8 @@ import { getRelativeTimeString } from '../../lib/date';
import type { ResourceType } from '../../lib/resource-progress';
import { EmptyStream } from './EmptyStream';
import { ActivityTopicsModal } from './ActivityTopicsModal.tsx';
import {Book, BookOpen, ChevronsDown, ChevronsDownUp, ChevronsUp, ChevronsUpDown} from 'lucide-react';
import { ChevronsDown, ChevronsUp } from 'lucide-react';
import { ActivityTopicTitles } from './ActivityTopicTitles.tsx';
export const allowedActivityActionType = [
'in_progress',
@@ -21,7 +22,7 @@ export type UserStreamActivity = {
resourceSlug?: string;
isCustomResource?: boolean;
actionType: AllowedActivityActionType;
topicIds?: string[];
topicTitles?: string[];
createdAt: Date;
updatedAt: Date;
};
@@ -38,7 +39,9 @@ export function ActivityStream(props: ActivityStreamProps) {
useState<UserStreamActivity | null>(null);
const sortedActivities = activities
.filter((activity) => activity?.topicIds && activity.topicIds.length > 0)
.filter(
(activity) => activity?.topicTitles && activity.topicTitles.length > 0,
)
.sort((a, b) => {
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
})
@@ -57,8 +60,8 @@ export function ActivityStream(props: ActivityStreamProps) {
resourceId={selectedActivity.resourceId}
resourceType={selectedActivity.resourceType}
isCustomResource={selectedActivity.isCustomResource}
topicIds={selectedActivity.topicIds || []}
topicCount={selectedActivity.topicIds?.length || 0}
topicTitles={selectedActivity.topicTitles || []}
topicCount={selectedActivity.topicTitles?.length || 0}
actionType={selectedActivity.actionType}
/>
)}
@@ -73,7 +76,7 @@ export function ActivityStream(props: ActivityStreamProps) {
resourceTitle,
actionType,
updatedAt,
topicIds,
topicTitles,
isCustomResource,
} = activity;
@@ -96,7 +99,7 @@ export function ActivityStream(props: ActivityStreamProps) {
</a>
);
const topicCount = topicIds?.length || 0;
const topicCount = topicTitles?.length || 0;
const timeAgo = (
<span className="ml-1 text-xs text-gray-400">
@@ -109,24 +112,20 @@ export function ActivityStream(props: ActivityStreamProps) {
{actionType === 'in_progress' && (
<>
Started{' '}
<button
className="font-medium underline underline-offset-2 hover:text-black"
onClick={() => setSelectedActivity(activity)}
>
{topicCount} topic{topicCount > 1 ? 's' : ''}
</button>{' '}
<ActivityTopicTitles
topicTitles={topicTitles || []}
onSelectActivity={() => setSelectedActivity(activity)}
/>{' '}
in {resourceLinkComponent} {timeAgo}
</>
)}
{actionType === 'done' && (
<>
Completed{' '}
<button
className="font-medium underline underline-offset-2 hover:text-black"
onClick={() => setSelectedActivity(activity)}
>
{topicCount} topic{topicCount > 1 ? 's' : ''}
</button>{' '}
<ActivityTopicTitles
topicTitles={topicTitles || []}
onSelectActivity={() => setSelectedActivity(activity)}
/>{' '}
in {resourceLinkComponent} {timeAgo}
</>
)}
@@ -146,16 +145,20 @@ export function ActivityStream(props: ActivityStreamProps) {
{activities.length > 10 && (
<button
className="mt-3 gap-2 flex items-center rounded-md border border-black pl-1.5 pr-2 py-1 text-xs uppercase tracking-wide text-black transition-colors hover:border-black hover:bg-black hover:text-white"
className="mt-3 flex items-center gap-2 rounded-md border border-black py-1 pl-1.5 pr-2 text-xs uppercase tracking-wide text-black transition-colors hover:border-black hover:bg-black hover:text-white"
onClick={() => setShowAll(!showAll)}
>
{showAll ? <>
<ChevronsUp size={14} />
Show less
</> : <>
<ChevronsDown size={14} />
Show more
</>}
{showAll ? (
<>
<ChevronsUp size={14} />
Show less
</>
) : (
<>
<ChevronsDown size={14} />
Show more
</>
)}
</button>
)}
</div>

View File

@@ -0,0 +1,41 @@
type ActivityTopicTitlesProps = {
topicTitles: string[];
onSelectActivity?: () => void;
};
export function ActivityTopicTitles(props: ActivityTopicTitlesProps) {
const { topicTitles, onSelectActivity } = props;
const firstThreeTopics = topicTitles?.slice(0, 3);
const remainingTopics = topicTitles?.slice(3);
return (
<>
{firstThreeTopics.map((topicTitle, index) => {
return (
<span className="font-medium">
<>
{index > 0 && ', '}
{index === firstThreeTopics.length - 1 &&
firstThreeTopics.length > 1 &&
'and '}
{topicTitle}
</>
</span>
);
})}
{remainingTopics?.length > 0 && (
<>
&nbsp;and&nbsp;
<button
className="font-medium underline underline-offset-2 hover:text-black"
onClick={onSelectActivity}
>
{remainingTopics.length} more topic
{remainingTopics.length > 1 ? 's' : ''}
</button>
</>
)}
</>
);
}

View File

@@ -11,7 +11,7 @@ type ActivityTopicDetailsProps = {
resourceId: string;
resourceType: ResourceType | 'question';
isCustomResource?: boolean;
topicIds: string[];
topicTitles: string[];
topicCount: number;
actionType: AllowedActivityActionType;
onClose: () => void;
@@ -22,56 +22,12 @@ export function ActivityTopicsModal(props: ActivityTopicDetailsProps) {
resourceId,
resourceType,
isCustomResource,
topicIds = [],
topicTitles = [],
topicCount,
actionType,
onClose,
} = props;
const [isLoading, setIsLoading] = useState(true);
const [topicTitles, setTopicTitles] = useState<Record<string, string>>({});
const [error, setError] = useState<string | null>(null);
const loadTopicTitles = async () => {
setIsLoading(true);
setError(null);
const { response, error } = await httpPost(
`${import.meta.env.PUBLIC_API_URL}/v1-get-topic-titles`,
{
resourceId,
resourceType,
isCustomResource,
topicIds,
},
);
if (error || !response) {
setError(error?.message || 'Failed to load topic titles');
setIsLoading(false);
return;
}
setTopicTitles(response);
setIsLoading(false);
};
useEffect(() => {
loadTopicTitles().finally(() => {
setIsLoading(false);
});
}, []);
if (isLoading || error) {
return (
<ModalLoader
error={error!}
text={'Loading topics..'}
isLoading={isLoading}
/>
);
}
let pageUrl = '';
if (resourceType === 'roadmap') {
pageUrl = isCustomResource ? `/r/${resourceId}` : `/${resourceId}`;
@@ -85,8 +41,6 @@ export function ActivityTopicsModal(props: ActivityTopicDetailsProps) {
<Modal
onClose={() => {
onClose();
setError(null);
setIsLoading(false);
}}
>
<div className={`popup-body relative rounded-lg bg-white p-4 shadow`}>
@@ -108,9 +62,7 @@ export function ActivityTopicsModal(props: ActivityTopicDetailsProps) {
</a>
</span>
<ul className="flex max-h-[50vh] flex-col gap-1 overflow-y-auto max-md:max-h-full">
{topicIds.map((topicId) => {
const topicTitle = topicTitles[topicId] || 'Unknown Topic';
{topicTitles.map((topicTitle) => {
const ActivityIcon =
actionType === 'done'
? Check
@@ -119,7 +71,7 @@ export function ActivityTopicsModal(props: ActivityTopicDetailsProps) {
: Check;
return (
<li key={topicId} className="flex items-start gap-2">
<li key={topicTitle} className="flex items-start gap-2">
<ActivityIcon
strokeWidth={3}
className="relative top-[4px] text-green-500"

View File

@@ -9,7 +9,7 @@ import { pageProgressMessage } from '../../stores/page';
import type { TeamResourceConfig } from './RoadmapSelector';
import { Step3 } from './Step3';
import { Step4 } from './Step4';
import {useToast} from "../../hooks/use-toast";
import { useToast } from '../../hooks/use-toast';
export interface TeamDocument {
_id?: string;
@@ -22,6 +22,7 @@ export interface TeamDocument {
linkedIn?: string;
};
type: ValidTeamType;
personalProgressOnly?: boolean;
canMemberSendInvite: boolean;
teamSize?: ValidTeamSize;
createdAt: Date;
@@ -40,10 +41,10 @@ export function CreateTeamForm() {
async function loadTeam(
teamIdToFetch: string,
requiredStepIndex: number | string
requiredStepIndex: number | string,
) {
const { response, error } = await httpGet<TeamDocument>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-team/${teamIdToFetch}`
`${import.meta.env.PUBLIC_API_URL}/v1-get-team/${teamIdToFetch}`,
);
if (error || !response) {
@@ -70,7 +71,7 @@ export function CreateTeamForm() {
async function loadTeamResourceConfig(teamId: string) {
const { error, response } = await httpGet<TeamResourceConfig>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-team-resource-config/${teamId}`
`${import.meta.env.PUBLIC_API_URL}/v1-get-team-resource-config/${teamId}`,
);
if (error || !Array.isArray(response)) {
console.error(error);
@@ -96,7 +97,7 @@ export function CreateTeamForm() {
}, [teamId, queryStepIndex]);
const [selectedTeamType, setSelectedTeamType] = useState<ValidTeamType>(
team?.type || 'company'
team?.type || 'company',
);
const [completedSteps, setCompletedSteps] = useState([0]);
@@ -191,13 +192,17 @@ export function CreateTeamForm() {
return (
<div className={'mx-auto max-w-[700px] py-1 md:py-6'}>
<div className={'mb-3 md:mb-8 pb-3 md:pb-0 border-b md:border-b-0 flex flex-col items-start md:items-center'}>
<h1 className={'text-xl md:text-4xl font-bold'}>Create Team</h1>
<p className={'mt-1 md:mt-2 text-sm md:text-base text-gray-500'}>
<div
className={
'mb-3 flex flex-col items-start border-b pb-3 md:mb-8 md:items-center md:border-b-0 md:pb-0'
}
>
<h1 className={'text-xl font-bold md:text-4xl'}>Create Team</h1>
<p className={'mt-1 text-sm text-gray-500 md:mt-2 md:text-base'}>
Complete the steps below to create your team
</p>
</div>
<div className="mb-8 mt-8 hidden sm:flex w-full">
<div className="mb-8 mt-8 hidden w-full sm:flex">
<Stepper
activeIndex={stepIndex}
completeSteps={completedSteps}

View File

@@ -46,7 +46,7 @@ export function Step1(props: Step1Props) {
const [linkedInUrl, setLinkedInUrl] = useState(team?.links?.linkedIn || '');
const [gitHubUrl, setGitHubUrl] = useState(team?.links?.github || '');
const [teamSize, setTeamSize] = useState<ValidTeamSize>(
team?.teamSize || ('' as any)
team?.teamSize || ('' as any),
);
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
@@ -74,7 +74,7 @@ export function Step1(props: Step1Props) {
}),
roadmapIds: [],
bestPracticeIds: [],
}
},
));
if (error || !response?._id) {
@@ -96,7 +96,7 @@ export function Step1(props: Step1Props) {
teamSize,
linkedInUrl: linkedInUrl || undefined,
}),
}
},
));
if (error || (response as any)?.status !== 'ok') {
@@ -168,7 +168,10 @@ export function Step1(props: Step1Props) {
{selectedTeamType === 'company' && (
<div className="mt-4 flex w-full flex-col">
<label htmlFor="website" className="text-sm leading-none text-slate-500">
<label
htmlFor="website"
className="text-sm leading-none text-slate-500"
>
Company LinkedIn URL
</label>
<input
@@ -187,7 +190,10 @@ export function Step1(props: Step1Props) {
)}
<div className="mt-4 flex w-full flex-col">
<label htmlFor="website" className="text-sm leading-none text-slate-500">
<label
htmlFor="website"
className="text-sm leading-none text-slate-500"
>
GitHub Organization URL
</label>
<input
@@ -221,11 +227,11 @@ export function Step1(props: Step1Props) {
setTeamSize((e.target as HTMLSelectElement).value as any)
}
>
<option value="">
Select team size
</option>
<option value="">Select team size</option>
{validTeamSizes.map((size) => (
<option key={size} value={size}>{size} people</option>
<option key={size} value={size}>
{size} people
</option>
))}
</select>
</div>

View File

@@ -1,4 +1,3 @@
import { Modal } from '../Modal.tsx';
import { useEffect, useState } from 'react';
import { deleteOpenAIKey, getOpenAIKey, saveOpenAIKey } from '../../lib/jwt.ts';
import { cn } from '../../lib/classname.ts';
@@ -17,7 +16,7 @@ export function OpenAISettings(props: OpenAISettingsProps) {
const [defaultOpenAIKey, setDefaultOpenAIKey] = useState('');
const [hasError, setHasError] = useState(false);
const [error, setError] = useState('');
const [openaiApiKey, setOpenaiApiKey] = useState('');
const [isLoading, setIsLoading] = useState(false);
@@ -57,7 +56,7 @@ export function OpenAISettings(props: OpenAISettingsProps) {
className="mt-4"
onSubmit={async (e) => {
e.preventDefault();
setHasError(false);
setError('');
const normalizedKey = openaiApiKey.trim();
if (!normalizedKey) {
@@ -68,7 +67,7 @@ export function OpenAISettings(props: OpenAISettingsProps) {
}
if (!normalizedKey.startsWith('sk-')) {
setHasError(true);
setError("Invalid OpenAI API key. It should start with 'sk-'");
return;
}
@@ -81,7 +80,7 @@ export function OpenAISettings(props: OpenAISettingsProps) {
);
if (error) {
setHasError(true);
setError(error.message);
setIsLoading(false);
return;
}
@@ -100,13 +99,13 @@ export function OpenAISettings(props: OpenAISettingsProps) {
className={cn(
'block w-full rounded-md border border-gray-300 px-3 py-2 text-gray-800 transition-colors focus:border-black focus:outline-none',
{
'border-red-500 bg-red-100 focus:border-red-500': hasError,
'border-red-500 bg-red-100 focus:border-red-500': error,
},
)}
placeholder="Enter your OpenAI API key"
value={openaiApiKey}
onChange={(e) => {
setHasError(false);
setError('');
setOpenaiApiKey((e.target as HTMLInputElement).value);
}}
/>
@@ -127,9 +126,9 @@ export function OpenAISettings(props: OpenAISettingsProps) {
We do not store your API key on our servers.
</p>
{hasError && (
{error && (
<p className="mt-2 text-sm text-red-500">
Please enter a valid OpenAI API key
{error}
</p>
)}
<button

View File

@@ -34,7 +34,7 @@ export function RoadCardPage() {
}
const badgeUrl = new URL(
`${import.meta.env.PUBLIC_API_URL}/v1-badge/${version}/${user?.id}`,
`${import.meta.env.PUBLIC_APP_URL}/card/${version}/${user?.id}`,
);
badgeUrl.searchParams.set('variant', variant);
@@ -146,7 +146,7 @@ export function RoadCardPage() {
className="flex cursor-pointer items-center justify-center rounded border border-gray-300 p-1.5 px-2 text-sm font-medium disabled:bg-blue-50"
onClick={() => copyText(badgeUrl.toString())}
>
<CopyIcon size={16} className="inline-block h-4 w-4 mr-1" />
<CopyIcon size={16} className="mr-1 inline-block h-4 w-4" />
{isCopied ? 'Copied!' : 'Copy Link'}
</button>

View File

@@ -2,6 +2,7 @@ import { useState } from 'react';
import { getRelativeTimeString } from '../../lib/date';
import type { TeamStreamActivity } from './TeamActivityPage';
import { ChevronsDown, ChevronsUp } from 'lucide-react';
import { ActivityTopicTitles } from '../Activity/ActivityTopicTitles';
type TeamActivityItemProps = {
onTopicClick?: (activity: TeamStreamActivity) => void;
@@ -72,8 +73,8 @@ export function TeamActivityItem(props: TeamActivityItemProps) {
if (activities.length === 1) {
const activity = activities[0];
const { actionType, topicIds } = activity;
const topicCount = topicIds?.length || 0;
const { actionType, topicTitles } = activity;
const topicCount = topicTitles?.length || 0;
return (
<li
@@ -83,12 +84,10 @@ export function TeamActivityItem(props: TeamActivityItemProps) {
{actionType === 'in_progress' && (
<>
{username} started{' '}
<button
className="font-medium underline underline-offset-2 hover:text-black"
onClick={() => onTopicClick?.(activity)}
>
{topicCount} topic{topicCount > 1 ? 's' : ''}
</button>{' '}
<ActivityTopicTitles
topicTitles={topicTitles || []}
onSelectActivity={() => onTopicClick?.(activity)}
/>{' '}
in {resourceLink(activity)} {timeAgo(activity.updatedAt)}
</>
)}
@@ -96,12 +95,10 @@ export function TeamActivityItem(props: TeamActivityItemProps) {
{actionType === 'done' && (
<>
{username} completed{' '}
<button
className="font-medium underline underline-offset-2 hover:text-black"
onClick={() => onTopicClick?.(activity)}
>
{topicCount} topic{topicCount > 1 ? 's' : ''}
</button>{' '}
<ActivityTopicTitles
topicTitles={topicTitles || []}
onSelectActivity={() => onTopicClick?.(activity)}
/>{' '}
in {resourceLink(activity)} {timeAgo(activity.updatedAt)}
</>
)}
@@ -131,32 +128,28 @@ export function TeamActivityItem(props: TeamActivityItemProps) {
<div className="py-3">
<ul className="ml-2 flex flex-col gap-2 sm:ml-[36px]">
{activities.slice(0, activityLimit).map((activity) => {
const { actionType, topicIds } = activity;
const topicCount = topicIds?.length || 0;
const { actionType, topicTitles } = activity;
const topicCount = topicTitles?.length || 0;
return (
<li key={activity._id} className="text-sm text-gray-600">
{actionType === 'in_progress' && (
<>
Started{' '}
<button
className="font-medium underline underline-offset-2 hover:text-black"
onClick={() => onTopicClick?.(activity)}
>
{topicCount} topic{topicCount > 1 ? 's' : ''}
</button>{' '}
<ActivityTopicTitles
topicTitles={topicTitles || []}
onSelectActivity={() => onTopicClick?.(activity)}
/>{' '}
in {resourceLink(activity)} {timeAgo(activity.updatedAt)}
</>
)}
{actionType === 'done' && (
<>
Completed{' '}
<button
className="font-medium underline underline-offset-2 hover:text-black"
onClick={() => onTopicClick?.(activity)}
>
{topicCount} topic{topicCount > 1 ? 's' : ''}
</button>{' '}
<ActivityTopicTitles
topicTitles={topicTitles || []}
onSelectActivity={() => onTopicClick?.(activity)}
/>{' '}
in {resourceLink(activity)} {timeAgo(activity.updatedAt)}
</>
)}

View File

@@ -18,7 +18,7 @@ export type TeamStreamActivity = {
resourceSlug?: string;
isCustomResource?: boolean;
actionType: AllowedActivityActionType;
topicIds?: string[];
topicTitles?: string[];
createdAt: Date;
updatedAt: Date;
};
@@ -98,38 +98,70 @@ export function TeamActivityPage() {
}, [teamId]);
const { users, activities } = teamActivities?.data;
const usersWithActivities = useMemo(() => {
const validActivities = activities.filter((activity) => {
const validActivities = useMemo(() => {
return activities?.filter((activity) => {
return (
activity.activity.length > 0 &&
activity.activity.some((t) => (t?.topicIds?.length || 0) > 0)
activity.activity.some((t) => (t?.topicTitles?.length || 0) > 0)
);
});
}, [activities]);
return users
.map((user) => {
const userActivities = validActivities
.filter((activity) => activity.userId === user._id)
.flatMap((activity) => activity.activity)
.filter((activity) => (activity?.topicIds?.length || 0) > 0)
.sort((a, b) => {
return (
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
);
});
const sortedUniqueCreatedAt = useMemo(() => {
return new Set(
validActivities
?.map((activity) => new Date(activity.createdAt).setHours(0, 0, 0, 0))
.sort((a, b) => {
return new Date(b).getTime() - new Date(a).getTime();
}),
);
}, [validActivities]);
return {
...user,
activities: userActivities,
};
})
.filter((user) => user.activities.length > 0)
.sort((a, b) => {
return (
new Date(b.activities[0].updatedAt).getTime() -
new Date(a.activities[0].updatedAt).getTime()
);
});
const usersWithActivities = useMemo(() => {
const enrichedUsers: {
_id: string;
name: string;
avatar?: string;
username?: string;
activities: TeamStreamActivity[];
}[] = [];
for (const uniqueCreatedAt of sortedUniqueCreatedAt) {
const uniqueActivities = validActivities.filter(
(activity) =>
new Date(activity.createdAt).setHours(0, 0, 0, 0) === uniqueCreatedAt,
);
const usersWithUniqueActivities = users
.map((user) => {
const userActivities = uniqueActivities
.filter((activity) => activity.userId === user._id)
.flatMap((activity) => activity.activity)
.filter((activity) => (activity?.topicTitles?.length || 0) > 0)
.sort((a, b) => {
return (
new Date(b.updatedAt).getTime() -
new Date(a.updatedAt).getTime()
);
});
return {
...user,
activities: userActivities,
};
})
.filter((user) => user.activities.length > 0)
.sort((a, b) => {
return (
new Date(b.activities[0].updatedAt).getTime() -
new Date(a.activities[0].updatedAt).getTime()
);
});
enrichedUsers.push(...usersWithUniqueActivities);
}
return enrichedUsers;
}, [users, activities]);
if (!teamId) {

View File

@@ -16,54 +16,10 @@ export function TeamActivityTopicsModal(props: TeamActivityTopicsModalProps) {
resourceId,
resourceType,
isCustomResource,
topicIds = [],
topicTitles = [],
actionType,
} = activity;
const [isLoading, setIsLoading] = useState(true);
const [topicTitles, setTopicTitles] = useState<Record<string, string>>({});
const [error, setError] = useState<string | null>(null);
const loadTopicTitles = async () => {
setIsLoading(true);
setError(null);
const { response, error } = await httpPost(
`${import.meta.env.PUBLIC_API_URL}/v1-get-topic-titles`,
{
resourceId,
resourceType,
isCustomResource,
topicIds,
},
);
if (error || !response) {
setError(error?.message || 'Failed to load topic titles');
setIsLoading(false);
return;
}
setTopicTitles(response);
setIsLoading(false);
};
useEffect(() => {
loadTopicTitles().finally(() => {
setIsLoading(false);
});
}, []);
if (isLoading || error) {
return (
<ModalLoader
error={error!}
text={'Loading topics..'}
isLoading={isLoading}
/>
);
}
let pageUrl = '';
if (resourceType === 'roadmap') {
pageUrl = isCustomResource ? `/r/${resourceId}` : `/${resourceId}`;
@@ -77,8 +33,6 @@ export function TeamActivityTopicsModal(props: TeamActivityTopicsModalProps) {
<Modal
onClose={() => {
onClose();
setError(null);
setIsLoading(false);
}}
>
<div className={`popup-body relative rounded-lg bg-white p-4 shadow`}>
@@ -100,9 +54,7 @@ export function TeamActivityTopicsModal(props: TeamActivityTopicsModalProps) {
</a>
</span>
<ul className="flex max-h-[50vh] flex-col gap-1 overflow-y-auto max-md:max-h-full">
{topicIds.map((topicId) => {
const topicTitle = topicTitles[topicId] || 'Unknown Topic';
{topicTitles.map((topicTitle) => {
const ActivityIcon =
actionType === 'done'
? Check
@@ -111,7 +63,7 @@ export function TeamActivityTopicsModal(props: TeamActivityTopicsModalProps) {
: Check;
return (
<li key={topicId} className="flex items-start gap-2">
<li key={topicTitle} className="flex items-start gap-2">
<ActivityIcon
strokeWidth={3}
className="relative top-[4px] text-green-500"

View File

@@ -24,6 +24,7 @@ export function UpdateTeamForm() {
const [gitHub, setGitHub] = useState('');
const [teamType, setTeamType] = useState('');
const [teamSize, setTeamSize] = useState('');
const [personalProgressOnly, setPersonalProgressOnly] = useState(false);
const validTeamSizes = [
'0-1',
'2-10',
@@ -55,11 +56,12 @@ export function UpdateTeamForm() {
website,
type: teamType,
gitHubUrl: gitHub || undefined,
personalProgressOnly,
...(teamType === 'company' && {
teamSize,
linkedInUrl: linkedIn || undefined,
}),
}
},
);
if (error) {
@@ -77,7 +79,7 @@ export function UpdateTeamForm() {
async function loadTeam() {
const { response, error } = await httpGet<TeamDocument>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-team/${teamId}`
`${import.meta.env.PUBLIC_API_URL}/v1-get-team/${teamId}`,
);
if (error || !response) {
console.log(error);
@@ -90,6 +92,7 @@ export function UpdateTeamForm() {
setLinkedIn(response?.links?.linkedIn || '');
setGitHub(response?.links?.github || '');
setTeamType(response.type);
setPersonalProgressOnly(response.personalProgressOnly ?? false);
if (response.teamSize) {
setTeamSize(response.teamSize);
}
@@ -205,16 +208,14 @@ export function UpdateTeamForm() {
<select
name="type"
id="type"
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
className="mt-2 block h-[42px] w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
disabled={isDisabled}
value={teamType || ''}
onChange={(e) =>
setTeamType((e.target as HTMLSelectElement).value as any)
}
>
<option value="">
Select type
</option>
<option value="">Select type</option>
<option value="company">Company</option>
<option value="study_group">Study Group</option>
</select>
@@ -231,7 +232,7 @@ export function UpdateTeamForm() {
<select
name="team-size"
id="team-size"
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
className="mt-2 block h-[42px] w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
required={teamType === 'company'}
disabled={isDisabled}
value={teamSize}
@@ -249,6 +250,31 @@ export function UpdateTeamForm() {
</div>
)}
<div className="mt-4 flex h-[42px] w-full items-center rounded-lg border border-gray-300 px-3 py-2 shadow-sm">
<label
htmlFor="personal-progress-only"
className="flex items-center gap-2 text-sm leading-none text-slate-500"
>
<input
type="checkbox"
name="personal-progress-only"
id="personal-progress-only"
disabled={isDisabled}
checked={personalProgressOnly}
onChange={(e) =>
setPersonalProgressOnly((e.target as HTMLInputElement).checked)
}
/>
<span>Members can only see their personal progress</span>
</label>
</div>
{personalProgressOnly && (
<p className="mt-2 rounded-lg border border-orange-300 bg-orange-50 p-2 text-sm text-orange-700">
Only admins and managers will be able to see the progress of members
</p>
)}
<div className="mt-4 flex w-full flex-col">
<button
type="submit"

View File

@@ -6,7 +6,7 @@ excludedBySlug: '/java/developer-skills'
seo:
title: 'Must-Have Java Full-stack Developer Skills in @currentYear@'
description: 'Master the essential skills every Java full stack developer needs. Boost your career with our expert tips!'
ogImageUrl: 'https://assets.roadmap.sh/guest/java-full-stack-developer-skills-yctex.png'
ogImageUrl: 'https://assets.roadmap.sh/guest/java-full-stack-developer-skills-sjzbd.png'
isNew: true
type: 'textual'
date: 2024-05-01

View File

@@ -1,3 +1,5 @@
# Insertion Sort
Insertion sort is a simple sorting algorithm that builds the final sorted array (or list) one item at a time. It's much less efficient on large lists than more advanced algorithms like quicksort, heapsort, or merge sort. Still, it provides several advantages such as it's easy to understand the algorithm, it performs well with small lists or lists that are already partially sorted and it can sort the list as it receives it. The algorithm iterates, consuming one input element each repetition and growing a sorted output list. At each iteration, it removes one element from the input data, finds the location it belongs within the sorted list and inserts it there. It repeats until no input elements remain.
Insertion sort is a simple sorting algorithm that builds the final sorted array (or list) one item at a time. It's much less efficient on large lists than more advanced algorithms like quicksort, heapsort, or merge sort. Still, it provides several advantages such as it's easy to understand the algorithm, it performs well with small lists or lists that are already partially sorted and it can sort the list as it receives it. The algorithm iterates, consuming one input element each repetition and growing a sorted output list. At each iteration, it removes one element from the input data, finds the location it belongs within the sorted list and inserts it there. It repeats until no input elements remain.
- [Insertion Sort - W3Schools](https://www.w3schools.com/dsa/dsa_algo_insertionsort.php)

View File

@@ -5,4 +5,3 @@ It is a feature in Kubernetes that automatically scales the number of replicas o
Learn more from the following resources:
- [Horizontal Pod Autoscaling - Documentation](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/)
- [Kubernetes Horizontal Pod Autoscaling - Kubernetes Tutorials](https://www.youtube.com/watch?v=hm3jnETOoFo)

View File

@@ -9,7 +9,7 @@ The main benefits of using TypeScript include:
- Improved Maintainability
- Backwards Compatibility
Learn more from the folowing links:
Learn more from the following links:
- [Overview of TypeScript](https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html)
- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html)

1
src/env.d.ts vendored
View File

@@ -4,6 +4,7 @@
interface ImportMetaEnv {
GITHUB_SHA: string;
PUBLIC_API_URL: string;
PUBLIC_APP_URL: string;
PUBLIC_AVATAR_BASE_URL: string;
PUBLIC_EDITOR_APP_URL: string;
}

17
src/lib/road-card.ts Normal file
View File

@@ -0,0 +1,17 @@
export async function getRoadCard(
version: 'tall' | 'wide',
userId: string,
variant: 'dark' | 'light',
roadmaps: string = '',
) {
const url = new URL(
`${import.meta.env.PUBLIC_API_URL}/v1-badge/${version}/${userId}`,
);
url.searchParams.set('variant', variant);
if (roadmaps) {
url.searchParams.set('roadmaps', roadmaps);
}
const response = await fetch(url.toString());
return response.text();
}

View File

@@ -0,0 +1,34 @@
import type { APIRoute } from 'astro';
import { getDefaultOpenGraphImageBuffer } from '../../../lib/open-graph';
import { getRoadCard } from '../../../lib/road-card';
export const prerender = false;
type Params = {
version: 'tall' | 'wide';
userId: string;
};
export const GET: APIRoute<any, Params> = async (context) => {
const { userId, version } = context.params;
if (!userId || !version) {
const buffer = await getDefaultOpenGraphImageBuffer();
return new Response(buffer, {
headers: {
'Content-Type': 'image/png',
},
});
}
const searchParams = new URLSearchParams(context.url.searchParams);
const variant = (searchParams.get('variant') as 'dark' | 'light') || 'dark';
const roadmaps = searchParams.get('roadmaps') || '';
const svg = await getRoadCard(version, userId, variant, roadmaps);
return new Response(svg, {
headers: {
'Content-Type': 'image/svg+xml',
},
});
};

View File

@@ -1,6 +1,7 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "node",
"jsx": "react-jsx",
"jsxImportSource": "react"