Merge branch 'master' into fix/activity

fix/activity
Arik Chakma 7 months ago
commit 0264b88069
  1. 23
      src/components/CreateTeam/CreateTeamForm.tsx
  2. 24
      src/components/CreateTeam/Step1.tsx
  3. 17
      src/components/GenerateRoadmap/OpenAISettings.tsx
  4. 4
      src/components/RoadCard/RoadCardPage.tsx
  5. 42
      src/components/TeamActivity/TeamActivityPage.tsx
  6. 40
      src/components/TeamSettings/UpdateTeamForm.tsx
  7. 2
      src/data/guides/java-developer-skills.md
  8. 2
      src/data/roadmaps/datastructures-and-algorithms/content/105-sorting-algorithms/102-insertion-sort.md
  9. 1
      src/data/roadmaps/kubernetes/content/109-autoscaling/100-horizontal-pod-autoscaler.md
  10. 2
      src/data/roadmaps/typescript/content/100-typescript/index.md
  11. 1
      src/env.d.ts
  12. 17
      src/lib/road-card.ts
  13. 34
      src/pages/card/[version]/[userId].ts
  14. 1
      tsconfig.json

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

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

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

@ -34,7 +34,7 @@ export function RoadCardPage() {
} }
const badgeUrl = new URL( 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); 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" 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())} 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'} {isCopied ? 'Copied!' : 'Copy Link'}
</button> </button>

@ -98,23 +98,50 @@ export function TeamActivityPage() {
}, [teamId]); }, [teamId]);
const { users, activities } = teamActivities?.data; const { users, activities } = teamActivities?.data;
const usersWithActivities = useMemo(() => { const validActivities = useMemo(() => {
const validActivities = activities.filter((activity) => { return activities?.filter((activity) => {
return ( return (
activity.activity.length > 0 && activity.activity.length > 0 &&
activity.activity.some((t) => (t?.topicTitles?.length || 0) > 0) activity.activity.some((t) => (t?.topicTitles?.length || 0) > 0)
); );
}); });
}, [activities]);
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 users 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) => { .map((user) => {
const userActivities = validActivities const userActivities = uniqueActivities
.filter((activity) => activity.userId === user._id) .filter((activity) => activity.userId === user._id)
.flatMap((activity) => activity.activity) .flatMap((activity) => activity.activity)
.filter((activity) => (activity?.topicTitles?.length || 0) > 0) .filter((activity) => (activity?.topicTitles?.length || 0) > 0)
.sort((a, b) => { .sort((a, b) => {
return ( return (
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() new Date(b.updatedAt).getTime() -
new Date(a.updatedAt).getTime()
); );
}); });
@ -130,6 +157,11 @@ export function TeamActivityPage() {
new Date(a.activities[0].updatedAt).getTime() new Date(a.activities[0].updatedAt).getTime()
); );
}); });
enrichedUsers.push(...usersWithUniqueActivities);
}
return enrichedUsers;
}, [users, activities]); }, [users, activities]);
if (!teamId) { if (!teamId) {

@ -24,6 +24,7 @@ export function UpdateTeamForm() {
const [gitHub, setGitHub] = useState(''); const [gitHub, setGitHub] = useState('');
const [teamType, setTeamType] = useState(''); const [teamType, setTeamType] = useState('');
const [teamSize, setTeamSize] = useState(''); const [teamSize, setTeamSize] = useState('');
const [personalProgressOnly, setPersonalProgressOnly] = useState(false);
const validTeamSizes = [ const validTeamSizes = [
'0-1', '0-1',
'2-10', '2-10',
@ -55,11 +56,12 @@ export function UpdateTeamForm() {
website, website,
type: teamType, type: teamType,
gitHubUrl: gitHub || undefined, gitHubUrl: gitHub || undefined,
personalProgressOnly,
...(teamType === 'company' && { ...(teamType === 'company' && {
teamSize, teamSize,
linkedInUrl: linkedIn || undefined, linkedInUrl: linkedIn || undefined,
}), }),
} },
); );
if (error) { if (error) {
@ -77,7 +79,7 @@ export function UpdateTeamForm() {
async function loadTeam() { async function loadTeam() {
const { response, error } = await httpGet<TeamDocument>( 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) { if (error || !response) {
console.log(error); console.log(error);
@ -90,6 +92,7 @@ export function UpdateTeamForm() {
setLinkedIn(response?.links?.linkedIn || ''); setLinkedIn(response?.links?.linkedIn || '');
setGitHub(response?.links?.github || ''); setGitHub(response?.links?.github || '');
setTeamType(response.type); setTeamType(response.type);
setPersonalProgressOnly(response.personalProgressOnly ?? false);
if (response.teamSize) { if (response.teamSize) {
setTeamSize(response.teamSize); setTeamSize(response.teamSize);
} }
@ -205,16 +208,14 @@ export function UpdateTeamForm() {
<select <select
name="type" name="type"
id="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} disabled={isDisabled}
value={teamType || ''} value={teamType || ''}
onChange={(e) => onChange={(e) =>
setTeamType((e.target as HTMLSelectElement).value as any) setTeamType((e.target as HTMLSelectElement).value as any)
} }
> >
<option value=""> <option value="">Select type</option>
Select type
</option>
<option value="company">Company</option> <option value="company">Company</option>
<option value="study_group">Study Group</option> <option value="study_group">Study Group</option>
</select> </select>
@ -231,7 +232,7 @@ export function UpdateTeamForm() {
<select <select
name="team-size" name="team-size"
id="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'} required={teamType === 'company'}
disabled={isDisabled} disabled={isDisabled}
value={teamSize} value={teamSize}
@ -249,6 +250,31 @@ export function UpdateTeamForm() {
</div> </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"> <div className="mt-4 flex w-full flex-col">
<button <button
type="submit" type="submit"

@ -6,7 +6,7 @@ excludedBySlug: '/java/developer-skills'
seo: seo:
title: 'Must-Have Java Full-stack Developer Skills in @currentYear@' 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!' 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 isNew: true
type: 'textual' type: 'textual'
date: 2024-05-01 date: 2024-05-01

@ -1,3 +1,5 @@
# Insertion Sort # 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)

@ -5,4 +5,3 @@ It is a feature in Kubernetes that automatically scales the number of replicas o
Learn more from the following resources: Learn more from the following resources:
- [Horizontal Pod Autoscaling - Documentation](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/) - [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)

@ -9,7 +9,7 @@ The main benefits of using TypeScript include:
- Improved Maintainability - Improved Maintainability
- Backwards Compatibility - 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) - [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) - [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html)

1
src/env.d.ts vendored

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

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

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

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

Loading…
Cancel
Save