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. 84
      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 { 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}

@ -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>

@ -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

@ -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>

@ -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?.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]);
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 users
.map((user) => {
const userActivities = validActivities
.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()
);
});
return enrichedUsers;
}, [users, activities]);
if (!teamId) {

@ -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"

@ -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

@ -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 - 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:
- [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
- 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

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

@ -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",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "node",
"jsx": "react-jsx",
"jsxImportSource": "react"

Loading…
Cancel
Save