Compare commits

..

2 Commits

Author SHA1 Message Date
Arik Chakma
e3b64d7990 fix: default false 2024-05-03 00:00:49 +06:00
Arik Chakma
4df03197ac feat: team personal progress only 2024-05-02 20:48:29 +06:00
15 changed files with 176 additions and 169 deletions

View File

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

@@ -1,41 +0,0 @@
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;
topicTitles: string[];
topicIds: string[];
topicCount: number;
actionType: AllowedActivityActionType;
onClose: () => void;
@@ -22,12 +22,56 @@ export function ActivityTopicsModal(props: ActivityTopicDetailsProps) {
resourceId,
resourceType,
isCustomResource,
topicTitles = [],
topicIds = [],
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}`;
@@ -41,6 +85,8 @@ export function ActivityTopicsModal(props: ActivityTopicDetailsProps) {
<Modal
onClose={() => {
onClose();
setError(null);
setIsLoading(false);
}}
>
<div className={`popup-body relative rounded-lg bg-white p-4 shadow`}>
@@ -62,7 +108,9 @@ 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">
{topicTitles.map((topicTitle) => {
{topicIds.map((topicId) => {
const topicTitle = topicTitles[topicId] || 'Unknown Topic';
const ActivityIcon =
actionType === 'done'
? Check
@@ -71,7 +119,7 @@ export function ActivityTopicsModal(props: ActivityTopicDetailsProps) {
: Check;
return (
<li key={topicTitle} className="flex items-start gap-2">
<li key={topicId} className="flex items-start gap-2">
<ActivityIcon
strokeWidth={3}
className="relative top-[4px] text-green-500"

View File

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

View File

@@ -34,7 +34,7 @@ export function RoadCardPage() {
}
const badgeUrl = new URL(
`${import.meta.env.PUBLIC_APP_URL}/card/${version}/${user?.id}`,
`${import.meta.env.PUBLIC_API_URL}/v1-badge/${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="mr-1 inline-block h-4 w-4" />
<CopyIcon size={16} className="inline-block h-4 w-4 mr-1" />
{isCopied ? 'Copied!' : 'Copy Link'}
</button>

View File

@@ -2,7 +2,6 @@ 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;
@@ -73,8 +72,8 @@ export function TeamActivityItem(props: TeamActivityItemProps) {
if (activities.length === 1) {
const activity = activities[0];
const { actionType, topicTitles } = activity;
const topicCount = topicTitles?.length || 0;
const { actionType, topicIds } = activity;
const topicCount = topicIds?.length || 0;
return (
<li
@@ -84,10 +83,12 @@ export function TeamActivityItem(props: TeamActivityItemProps) {
{actionType === 'in_progress' && (
<>
{username} started{' '}
<ActivityTopicTitles
topicTitles={topicTitles || []}
onSelectActivity={() => onTopicClick?.(activity)}
/>{' '}
<button
className="font-medium underline underline-offset-2 hover:text-black"
onClick={() => onTopicClick?.(activity)}
>
{topicCount} topic{topicCount > 1 ? 's' : ''}
</button>{' '}
in {resourceLink(activity)} {timeAgo(activity.updatedAt)}
</>
)}
@@ -95,10 +96,12 @@ export function TeamActivityItem(props: TeamActivityItemProps) {
{actionType === 'done' && (
<>
{username} completed{' '}
<ActivityTopicTitles
topicTitles={topicTitles || []}
onSelectActivity={() => onTopicClick?.(activity)}
/>{' '}
<button
className="font-medium underline underline-offset-2 hover:text-black"
onClick={() => onTopicClick?.(activity)}
>
{topicCount} topic{topicCount > 1 ? 's' : ''}
</button>{' '}
in {resourceLink(activity)} {timeAgo(activity.updatedAt)}
</>
)}
@@ -128,28 +131,32 @@ 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, topicTitles } = activity;
const topicCount = topicTitles?.length || 0;
const { actionType, topicIds } = activity;
const topicCount = topicIds?.length || 0;
return (
<li key={activity._id} className="text-sm text-gray-600">
{actionType === 'in_progress' && (
<>
Started{' '}
<ActivityTopicTitles
topicTitles={topicTitles || []}
onSelectActivity={() => onTopicClick?.(activity)}
/>{' '}
<button
className="font-medium underline underline-offset-2 hover:text-black"
onClick={() => onTopicClick?.(activity)}
>
{topicCount} topic{topicCount > 1 ? 's' : ''}
</button>{' '}
in {resourceLink(activity)} {timeAgo(activity.updatedAt)}
</>
)}
{actionType === 'done' && (
<>
Completed{' '}
<ActivityTopicTitles
topicTitles={topicTitles || []}
onSelectActivity={() => onTopicClick?.(activity)}
/>{' '}
<button
className="font-medium underline underline-offset-2 hover:text-black"
onClick={() => onTopicClick?.(activity)}
>
{topicCount} topic{topicCount > 1 ? 's' : ''}
</button>{' '}
in {resourceLink(activity)} {timeAgo(activity.updatedAt)}
</>
)}

View File

@@ -18,7 +18,7 @@ export type TeamStreamActivity = {
resourceSlug?: string;
isCustomResource?: boolean;
actionType: AllowedActivityActionType;
topicTitles?: string[];
topicIds?: string[];
createdAt: Date;
updatedAt: Date;
};
@@ -102,7 +102,7 @@ export function TeamActivityPage() {
return activities?.filter((activity) => {
return (
activity.activity.length > 0 &&
activity.activity.some((t) => (t?.topicTitles?.length || 0) > 0)
activity.activity.some((t) => (t?.topicIds?.length || 0) > 0)
);
});
}, [activities]);
@@ -137,7 +137,7 @@ export function TeamActivityPage() {
const userActivities = uniqueActivities
.filter((activity) => activity.userId === user._id)
.flatMap((activity) => activity.activity)
.filter((activity) => (activity?.topicTitles?.length || 0) > 0)
.filter((activity) => (activity?.topicIds?.length || 0) > 0)
.sort((a, b) => {
return (
new Date(b.updatedAt).getTime() -

View File

@@ -16,10 +16,54 @@ export function TeamActivityTopicsModal(props: TeamActivityTopicsModalProps) {
resourceId,
resourceType,
isCustomResource,
topicTitles = [],
topicIds = [],
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}`;
@@ -33,6 +77,8 @@ export function TeamActivityTopicsModal(props: TeamActivityTopicsModalProps) {
<Modal
onClose={() => {
onClose();
setError(null);
setIsLoading(false);
}}
>
<div className={`popup-body relative rounded-lg bg-white p-4 shadow`}>
@@ -54,7 +100,9 @@ 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">
{topicTitles.map((topicTitle) => {
{topicIds.map((topicId) => {
const topicTitle = topicTitles[topicId] || 'Unknown Topic';
const ActivityIcon =
actionType === 'done'
? Check
@@ -63,7 +111,7 @@ export function TeamActivityTopicsModal(props: TeamActivityTopicsModalProps) {
: Check;
return (
<li key={topicTitle} className="flex items-start gap-2">
<li key={topicId} className="flex items-start gap-2">
<ActivityIcon
strokeWidth={3}
className="relative top-[4px] text-green-500"

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-sjzbd.png'
ogImageUrl: 'https://assets.roadmap.sh/guest/java-full-stack-developer-skills-yctex.png'
isNew: true
type: 'textual'
date: 2024-05-01

View File

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

View File

@@ -5,3 +5,4 @@ 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 following links:
Learn more from the folowing 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,7 +4,6 @@
interface ImportMetaEnv {
GITHUB_SHA: string;
PUBLIC_API_URL: string;
PUBLIC_APP_URL: string;
PUBLIC_AVATAR_BASE_URL: string;
PUBLIC_EDITOR_APP_URL: string;
}

View File

@@ -1,17 +0,0 @@
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

@@ -1,34 +0,0 @@
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',
},
});
};