chore: migrate from preact to react

chore/preact-to-react
Kamran Ahmed 1 year ago
parent c274feced1
commit bdbb955858
  1. 7
      astro.config.mjs
  2. 9
      package.json
  3. 713
      pnpm-lock.yaml
  4. 10
      src/components/Activity/ActivityCounters.tsx
  5. 9
      src/components/Activity/ActivityPage.tsx
  6. 12
      src/components/Activity/EmptyActivity.tsx
  7. 2
      src/components/Activity/ResourceProgress.tsx
  8. 30
      src/components/AddTeamRoadmap.tsx
  9. 10
      src/components/AuthenticationFlow/EmailLoginForm.tsx
  10. 2
      src/components/AuthenticationFlow/EmailSignupForm.tsx
  11. 4
      src/components/AuthenticationFlow/ForgotPasswordForm.tsx
  12. 8
      src/components/AuthenticationFlow/GitHubButton.tsx
  13. 8
      src/components/AuthenticationFlow/GoogleButton.tsx
  14. 8
      src/components/AuthenticationFlow/LinkedInButton.tsx
  15. 2
      src/components/AuthenticationFlow/ResetPasswordForm.tsx
  16. 6
      src/components/AuthenticationFlow/TriggerVerifyAccount.tsx
  17. 16
      src/components/AuthenticationFlow/VerificationEmailMessage.tsx
  18. 38
      src/components/Befriend.tsx
  19. 28
      src/components/CommandMenu/CommandMenu.tsx
  20. 2
      src/components/CreateTeam/CreateTeamForm.tsx
  21. 2
      src/components/CreateTeam/RoadmapSelector.tsx
  22. 2
      src/components/CreateTeam/RoleDropdown.tsx
  23. 10
      src/components/CreateTeam/SelectRoadmapModal.tsx
  24. 2
      src/components/CreateTeam/Step0.tsx
  25. 4
      src/components/CreateTeam/Step1.tsx
  26. 10
      src/components/CreateTeam/Step3.tsx
  27. 8
      src/components/CreateTeam/Step4.tsx
  28. 12
      src/components/CreateTeam/UpdateTeamResourceModal.tsx
  29. 2
      src/components/DeleteAccount/DeleteAccountForm.tsx
  30. 2
      src/components/DeleteAccount/DeleteAccountPopup.astro
  31. 12
      src/components/DeleteTeamPopup.tsx
  32. 8
      src/components/FeaturedItems/FavoriteIcon.tsx
  33. 2
      src/components/FeaturedItems/FeaturedItem.astro
  34. 8
      src/components/FeaturedItems/MarkFavorite.tsx
  35. 19
      src/components/Feedback/SubmitFeedbackPopup.tsx
  36. 20
      src/components/Friends/EmptyFriends.tsx
  37. 12
      src/components/Friends/FriendProgressItem.tsx
  38. 5
      src/components/Friends/FriendsPage.tsx
  39. 12
      src/components/Friends/InviteFriendPopup.tsx
  40. 10
      src/components/Friends/SidebarFriendsCounter.tsx
  41. 10
      src/components/HeroSection/FavoriteRoadmaps.tsx
  42. 2
      src/components/HeroSection/HeroSection.astro
  43. 2
      src/components/HeroSection/ProgressList.tsx
  44. 4
      src/components/Notification/NotificationPage.tsx
  45. 8
      src/components/PageProgress.tsx
  46. 27
      src/components/PageSponsor.tsx
  47. 6
      src/components/ReactIcons/AddUserIcon.tsx
  48. 6
      src/components/ReactIcons/AddedUserIcon.tsx
  49. 2
      src/components/ReactIcons/CheckIcon.tsx
  50. 6
      src/components/ReactIcons/ChevronDownIcon.tsx
  51. 2
      src/components/ReactIcons/CloseIcon.tsx
  52. 6
      src/components/ReactIcons/DeleteUserIcon.tsx
  53. 18
      src/components/ReactIcons/ErrorIcon.tsx
  54. 18
      src/components/ReactIcons/InfoIcon.tsx
  55. 6
      src/components/ReactIcons/MailIcon.tsx
  56. 6
      src/components/ReactIcons/ShareIcon.tsx
  57. 9
      src/components/ReactIcons/Spinner.tsx
  58. 6
      src/components/ReactIcons/StopIcon.tsx
  59. 6
      src/components/ReactIcons/TrashIcon.tsx
  60. 18
      src/components/ReactIcons/WarningIcon.tsx
  61. 4
      src/components/ResourceProgressStats.astro
  62. 12
      src/components/RespondInviteForm.tsx
  63. 5
      src/components/RoadCard/Editor.tsx
  64. 4
      src/components/RoadCard/RoadCardPage.tsx
  65. 15
      src/components/RoadCard/RoadmapSelect.tsx
  66. 2
      src/components/RoadmapHeader.astro
  67. 8
      src/components/SearchSelector.tsx
  68. 2
      src/components/Stepper.tsx
  69. 16
      src/components/TeamDropdown/TeamDropdown.tsx
  70. 20
      src/components/TeamMembers/InviteMemberPopup.tsx
  71. 2
      src/components/TeamMembers/LeaveTeamButton.tsx
  72. 10
      src/components/TeamMembers/LeaveTeamPopup.tsx
  73. 2
      src/components/TeamMembers/MemberActionDropdown.tsx
  74. 4
      src/components/TeamMembers/TeamMemberItem.tsx
  75. 4
      src/components/TeamMembers/TeamMembersPage.tsx
  76. 22
      src/components/TeamMembers/UpdateMemberPopup.tsx
  77. 2
      src/components/TeamProgress/GroupRoadmapItem.tsx
  78. 4
      src/components/TeamProgress/MemberProgressItem.tsx
  79. 28
      src/components/TeamProgress/MemberProgressModal.tsx
  80. 28
      src/components/TeamProgress/TeamProgressPage.tsx
  81. 6
      src/components/TeamRoadmaps.tsx
  82. 36
      src/components/TeamSettings/UpdateTeamForm.tsx
  83. 51
      src/components/TeamSidebar.tsx
  84. 24
      src/components/TeamVersions/TeamVersions.tsx
  85. 10
      src/components/TeamsList.tsx
  86. 4
      src/components/Toast.tsx
  87. 4
      src/components/TopicDetail/ContributionForm.tsx
  88. 10
      src/components/TopicDetail/TopicDetail.tsx
  89. 34
      src/components/TopicDetail/TopicProgressButton.tsx
  90. 23
      src/components/UpdatePassword/UpdatePasswordForm.tsx
  91. 28
      src/components/UpdateProfile/UpdateProfileForm.tsx
  92. 6
      src/components/UpdateProfile/UploadProfilePicture.tsx
  93. 1
      src/components/UserProgress/ProgressShareButton.tsx
  94. 42
      src/components/UserProgress/UserProgressModal.tsx
  95. 2
      src/hooks/use-copy-text.ts
  96. 2
      src/hooks/use-keydown.ts
  97. 2
      src/hooks/use-load-topic.ts
  98. 2
      src/hooks/use-outside-click.ts
  99. 2
      src/hooks/use-params.ts
  100. 2
      src/hooks/use-team-id.ts
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,5 +1,4 @@
// https://astro.build/config // https://astro.build/config
import preact from '@astrojs/preact';
import sitemap from '@astrojs/sitemap'; import sitemap from '@astrojs/sitemap';
import tailwind from '@astrojs/tailwind'; import tailwind from '@astrojs/tailwind';
import compress from 'astro-compress'; import compress from 'astro-compress';
@ -8,6 +7,8 @@ import rehypeExternalLinks from 'rehype-external-links';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { serializeSitemap, shouldIndexPage } from './sitemap.mjs'; import { serializeSitemap, shouldIndexPage } from './sitemap.mjs';
import react from '@astrojs/react';
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
site: 'https://roadmap.sh/', site: 'https://roadmap.sh/',
@ -31,11 +32,9 @@ export default defineConfig({
'https://cs.fyi', 'https://cs.fyi',
'https://roadmap.sh', 'https://roadmap.sh',
]; ];
if (whiteListedStarts.some((start) => href.startsWith(start))) { if (whiteListedStarts.some((start) => href.startsWith(start))) {
return []; return [];
} }
return 'noopener noreferrer nofollow'; return 'noopener noreferrer nofollow';
}, },
}, },
@ -75,6 +74,6 @@ export default defineConfig({
CSS: false, CSS: false,
JavaScript: false, JavaScript: false,
}), }),
preact(), react(),
], ],
}); });

@ -21,11 +21,13 @@
"test:e2e": "playwright test" "test:e2e": "playwright test"
}, },
"dependencies": { "dependencies": {
"@astrojs/preact": "^2.2.1", "@astrojs/react": "^2.2.2",
"@astrojs/sitemap": "^1.3.3", "@astrojs/sitemap": "^1.3.3",
"@astrojs/tailwind": "^3.1.3", "@astrojs/tailwind": "^3.1.3",
"@fingerprintjs/fingerprintjs": "^3.4.1", "@fingerprintjs/fingerprintjs": "^3.4.1",
"@nanostores/preact": "^0.5.0", "@nanostores/react": "^0.7.1",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"astro": "^2.6.6", "astro": "^2.6.6",
"astro-compress": "^2.0.8", "astro-compress": "^2.0.8",
"jose": "^4.14.4", "jose": "^4.14.4",
@ -33,7 +35,8 @@
"nanostores": "^0.9.2", "nanostores": "^0.9.2",
"node-html-parser": "^6.1.5", "node-html-parser": "^6.1.5",
"npm-check-updates": "^16.10.12", "npm-check-updates": "^16.10.12",
"preact": "^10.15.1", "react": "^18.0.0",
"react-dom": "^18.0.0",
"rehype-external-links": "^2.1.0", "rehype-external-links": "^2.1.0",
"roadmap-renderer": "^1.0.6", "roadmap-renderer": "^1.0.6",
"slugify": "^1.6.6", "slugify": "^1.6.6",

File diff suppressed because it is too large Load Diff

@ -21,11 +21,11 @@ function ActivityCounter(props: ActivityCounterType) {
const { text, count } = props; const { text, count } = props;
return ( return (
<div class="relative flex flex-1 flex-row-reverse sm:flex-col px-0 sm:px-4 py-2 sm:py-4 text-center sm:pt-10 items-center gap-2 sm:gap-0 justify-end"> <div className="relative flex flex-1 flex-row-reverse sm:flex-col px-0 sm:px-4 py-2 sm:py-4 text-center sm:pt-10 items-center gap-2 sm:gap-0 justify-end">
<h2 class="text-base sm:text-5xl font-bold"> <h2 className="text-base sm:text-5xl font-bold">
{count} {count}
</h2> </h2>
<p class="mt-0 sm:mt-2 text-sm text-gray-400">{text}</p> <p className="mt-0 sm:mt-2 text-sm text-gray-400">{text}</p>
</div> </div>
); );
} }
@ -34,8 +34,8 @@ export function ActivityCounters(props: ActivityCountersType) {
const { done, learning, streak } = props; const { done, learning, streak } = props;
return ( return (
<div class="mx-0 -mt-5 sm:-mx-10 md:-mt-10"> <div className="mx-0 -mt-5 sm:-mx-10 md:-mt-10">
<div class="flex flex-col sm:flex-row gap-0 sm:gap-2 divide-y sm:divide-y-0 divide-x-0 sm:divide-x border-b"> <div className="flex flex-col sm:flex-row gap-0 sm:gap-2 divide-y sm:divide-y-0 divide-x-0 sm:divide-x border-b">
<ActivityCounter <ActivityCounter
text={'Topics Completed'} text={'Topics Completed'}
count={`${done?.total || 0}`} count={`${done?.total || 0}`}

@ -1,4 +1,4 @@
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
import { httpGet } from '../../lib/http'; import { httpGet } from '../../lib/http';
import { ActivityCounters } from './ActivityCounters'; import { ActivityCounters } from './ActivityCounters';
import { ResourceProgress } from './ResourceProgress'; import { ResourceProgress } from './ResourceProgress';
@ -91,16 +91,16 @@ export function ActivityPage() {
streak={activity?.streak || { count: 0 }} streak={activity?.streak || { count: 0 }}
/> />
<div class="mx-0 px-0 py-5 md:-mx-10 md:px-8 md:py-8"> <div className="mx-0 px-0 py-5 md:-mx-10 md:px-8 md:py-8">
{learningRoadmaps.length === 0 && {learningRoadmaps.length === 0 &&
learningBestPractices.length === 0 && <EmptyActivity />} learningBestPractices.length === 0 && <EmptyActivity />}
{(learningRoadmaps.length > 0 || learningBestPractices.length > 0) && ( {(learningRoadmaps.length > 0 || learningBestPractices.length > 0) && (
<> <>
<h2 class="mb-3 text-xs uppercase text-gray-400"> <h2 className="mb-3 text-xs uppercase text-gray-400">
Continue Following Continue Following
</h2> </h2>
<div class="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{learningRoadmaps {learningRoadmaps
.sort((a, b) => { .sort((a, b) => {
const updatedAtA = new Date(a.updatedAt); const updatedAtA = new Date(a.updatedAt);
@ -110,6 +110,7 @@ export function ActivityPage() {
}) })
.map((roadmap) => ( .map((roadmap) => (
<ResourceProgress <ResourceProgress
key={roadmap.id}
doneCount={roadmap.done || 0} doneCount={roadmap.done || 0}
learningCount={roadmap.learning || 0} learningCount={roadmap.learning || 0}
totalCount={roadmap.total || 0} totalCount={roadmap.total || 0}

@ -2,21 +2,21 @@ import RoadmapIcon from '../../icons/roadmap.svg';
export function EmptyActivity() { export function EmptyActivity() {
return ( return (
<div class="rounded-md"> <div className="rounded-md">
<div class="flex flex-col items-center p-7 text-center"> <div className="flex flex-col items-center p-7 text-center">
<img <img
alt="no roadmaps" alt="no roadmaps"
src={RoadmapIcon} src={RoadmapIcon}
class="mb-2 w-[60px] h-[60px] sm:h-[120px] sm:w-[120px] opacity-10" className="mb-2 w-[60px] h-[60px] sm:h-[120px] sm:w-[120px] opacity-10"
/> />
<h2 class="text-lg sm:text-xl font-bold">No Progress</h2> <h2 className="text-lg sm:text-xl font-bold">No Progress</h2>
<p className="my-1 sm:my-2 max-w-[400px] text-gray-500 text-sm sm:text-base"> <p className="my-1 sm:my-2 max-w-[400px] text-gray-500 text-sm sm:text-base">
Progress will appear here as you start tracking your{' '} Progress will appear here as you start tracking your{' '}
<a href="/roadmaps" class="mt-4 text-blue-500 hover:underline"> <a href="/roadmaps" className="mt-4 text-blue-500 hover:underline">
Roadmaps Roadmaps
</a>{' '} </a>{' '}
or{' '} or{' '}
<a href="/best-practices" class="mt-4 text-blue-500 hover:underline"> <a href="/best-practices" className="mt-4 text-blue-500 hover:underline">
Best Practices Best Practices
</a>{' '} </a>{' '}
progress. progress.

@ -1,8 +1,8 @@
import { useState } from 'preact/hooks';
import { httpPost } from '../../lib/http'; import { httpPost } from '../../lib/http';
import { getRelativeTimeString } from '../../lib/date'; import { getRelativeTimeString } from '../../lib/date';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import { ProgressShareButton } from '../UserProgress/ProgressShareButton'; import { ProgressShareButton } from '../UserProgress/ProgressShareButton';
import { useState } from 'react';
type ResourceProgressType = { type ResourceProgressType = {
resourceType: 'roadmap' | 'best-practice'; resourceType: 'roadmap' | 'best-practice';

@ -1,4 +1,4 @@
import { useRef, useState } from 'preact/hooks'; import { useRef, useState } from 'react';
import { useOutsideClick } from '../hooks/use-outside-click'; import { useOutsideClick } from '../hooks/use-outside-click';
import { OptionType, SearchSelector } from './SearchSelector'; import { OptionType, SearchSelector } from './SearchSelector';
import type { PageType } from './CommandMenu/CommandMenu'; import type { PageType } from './CommandMenu/CommandMenu';
@ -65,15 +65,15 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
)?.title; )?.title;
return ( return (
<div class="popup fixed left-0 right-0 top-0 z-50 flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50"> <div className="popup fixed left-0 right-0 top-0 z-50 flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50">
<div class="relative h-full w-full max-w-md p-4 md:h-auto"> <div className="relative h-full w-full max-w-md p-4 md:h-auto">
<div <div
ref={popupBodyEl} ref={popupBodyEl}
class="popup-body relative rounded-lg bg-white p-4 shadow" className="popup-body relative rounded-lg bg-white p-4 shadow"
> >
{isLoading && ( {isLoading && (
<> <>
<div class="flex items-center justify-center gap-2 py-8"> <div className="flex items-center justify-center gap-2 py-8">
<Spinner isDualRing={false} className="h-4 w-4" /> <Spinner isDualRing={false} className="h-4 w-4" />
<h2 className="font-medium">Loading...</h2> <h2 className="font-medium">Loading...</h2>
</div> </div>
@ -82,7 +82,7 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
{!isLoading && !error && selectedRoadmap && ( {!isLoading && !error && selectedRoadmap && (
<div className={'text-center'}> <div className={'text-center'}>
<CheckIcon additionalClasses="h-10 w-10 mx-auto opacity-20 mb-3 mt-4" /> <CheckIcon additionalClasses="h-10 w-10 mx-auto opacity-20 mb-3 mt-4" />
<h3 class="mb-1.5 text-2xl font-medium"> <h3 className="mb-1.5 text-2xl font-medium">
{selectedRoadmapTitle} Added {selectedRoadmapTitle} Added
</h3> </h3>
<p className="mb-4 text-sm leading-none text-gray-400"> <p className="mb-4 text-sm leading-none text-gray-400">
@ -95,11 +95,11 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
to make changes to the roadmap. to make changes to the roadmap.
</p> </p>
<div class="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={onClose} onClick={onClose}
type="button" type="button"
class="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300" className="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300"
> >
Done Done
</button> </button>
@ -110,7 +110,7 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
setIsLoading(false); setIsLoading(false);
}} }}
type="button" type="button"
class="flex-grow cursor-pointer rounded-lg bg-black py-2 text-center text-white" className="flex-grow cursor-pointer rounded-lg bg-black py-2 text-center text-white"
> >
+ Add More + Add More
</button> </button>
@ -119,14 +119,14 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
)} )}
{!isLoading && error && ( {!isLoading && error && (
<> <>
<h3 class="mb-1.5 text-2xl font-medium">Error</h3> <h3 className="mb-1.5 text-2xl font-medium">Error</h3>
<p className="mb-3 text-sm leading-none text-red-400">{error}</p> <p className="mb-3 text-sm leading-none text-red-400">{error}</p>
<div class="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={onClose} onClick={onClose}
type="button" type="button"
class="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300" className="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300"
> >
Cancel Cancel
</button> </button>
@ -135,7 +135,7 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
)} )}
{!isLoading && !error && !selectedRoadmap && ( {!isLoading && !error && !selectedRoadmap && (
<> <>
<h3 class="mb-1.5 text-2xl font-medium">Add Roadmap</h3> <h3 className="mb-1.5 text-2xl font-medium">Add Roadmap</h3>
<p className="mb-3 text-sm leading-none text-gray-400"> <p className="mb-3 text-sm leading-none text-gray-400">
Search and add a roadmap Search and add a roadmap
</p> </p>
@ -156,11 +156,11 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
placeholder={'Search for roadmap'} placeholder={'Search for roadmap'}
/> />
<div class="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={onClose} onClick={onClose}
type="button" type="button"
class="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300" className="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300"
> >
Cancel Cancel
</button> </button>

@ -1,17 +1,17 @@
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import type { FunctionComponent } from 'preact'; import type { FormEvent } from 'react';
import { useState } from 'preact/hooks'; import { useState } from 'react';
import { httpPost } from '../../lib/http'; import { httpPost } from '../../lib/http';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt'; import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
const EmailLoginForm: FunctionComponent<{}> = () => { const EmailLoginForm = () => {
const [email, setEmail] = useState<string>(''); const [email, setEmail] = useState<string>('');
const [password, setPassword] = useState<string>(''); const [password, setPassword] = useState<string>('');
const [error, setError] = useState(''); const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const handleFormSubmit = async (e: Event) => { const handleFormSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
setIsLoading(true); setIsLoading(true);
setError(''); setError('');
@ -77,7 +77,7 @@ const EmailLoginForm: FunctionComponent<{}> = () => {
onInput={(e) => setPassword(String((e.target as any).value))} onInput={(e) => setPassword(String((e.target as any).value))}
/> />
<p class="mb-3 mt-2 text-sm text-gray-500"> <p className="mb-3 mt-2 text-sm text-gray-500">
<a <a
href="/forgot-password" href="/forgot-password"
className="text-blue-800 hover:text-blue-600" className="text-blue-800 hover:text-blue-600"

@ -1,5 +1,5 @@
import type { FunctionComponent } from 'preact'; import type { FunctionComponent } from 'preact';
import { useState } from 'preact/hooks'; import { useState } from 'react';
import { httpPost } from '../../lib/http'; import { httpPost } from '../../lib/http';
const EmailSignupForm: FunctionComponent = () => { const EmailSignupForm: FunctionComponent = () => {

@ -1,4 +1,4 @@
import { useState } from 'preact/hooks'; import { useState } from 'react';
import { httpPost } from '../../lib/http'; import { httpPost } from '../../lib/http';
export function ForgotPasswordForm() { export function ForgotPasswordForm() {
@ -29,7 +29,7 @@ export function ForgotPasswordForm() {
}; };
return ( return (
<form onSubmit={handleSubmit} class="w-full"> <form onSubmit={handleSubmit} className="w-full">
<input <input
type="email" type="email"
name="email" name="email"

@ -1,4 +1,4 @@
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
import GitHubIcon from '../../icons/github.svg'; import GitHubIcon from '../../icons/github.svg';
import SpinnerIcon from '../../icons/spinner.svg'; import SpinnerIcon from '../../icons/spinner.svg';
@ -106,14 +106,14 @@ export function GitHubButton(props: GitHubButtonProps) {
return ( return (
<> <>
<button <button
class="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60" className="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
disabled={isLoading} disabled={isLoading}
onClick={handleClick} onClick={handleClick}
> >
<img <img
src={icon} src={icon as any}
alt="GitHub" alt="GitHub"
class={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`} className={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
/> />
Continue with GitHub Continue with GitHub
</button> </button>

@ -1,4 +1,4 @@
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import GoogleIcon from '../../icons/google.svg'; import GoogleIcon from '../../icons/google.svg';
import SpinnerIcon from '../../icons/spinner.svg'; import SpinnerIcon from '../../icons/spinner.svg';
@ -106,14 +106,14 @@ export function GoogleButton(props: GoogleButtonProps) {
return ( return (
<> <>
<button <button
class="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60" className="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
disabled={isLoading} disabled={isLoading}
onClick={handleClick} onClick={handleClick}
> >
<img <img
src={icon} src={icon as any}
alt="Google" alt="Google"
class={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`} className={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
/> />
Continue with Google Continue with Google
</button> </button>

@ -1,4 +1,4 @@
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import LinkedIn from '../../icons/linkedin.svg'; import LinkedIn from '../../icons/linkedin.svg';
import SpinnerIcon from '../../icons/spinner.svg'; import SpinnerIcon from '../../icons/spinner.svg';
@ -106,14 +106,14 @@ export function LinkedInButton(props: LinkedInButtonProps) {
return ( return (
<> <>
<button <button
class="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60" className="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
disabled={isLoading} disabled={isLoading}
onClick={handleClick} onClick={handleClick}
> >
<img <img
src={icon} src={icon as any}
alt="Google" alt="Google"
class={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`} className={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
/> />
Continue with LinkedIn Continue with LinkedIn
</button> </button>

@ -1,4 +1,4 @@
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
import { httpPost } from '../../lib/http'; import { httpPost } from '../../lib/http';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt'; import { TOKEN_COOKIE_NAME } from '../../lib/jwt';

@ -1,7 +1,7 @@
import SpinnerIcon from '../../icons/spinner.svg'; import SpinnerIcon from '../../icons/spinner.svg';
import ErrorIcon from '../../icons/error.svg'; import ErrorIcon from '../../icons/error.svg';
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt'; import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
import { httpPost } from '../../lib/http'; import { httpPost } from '../../lib/http';
@ -60,7 +60,7 @@ export function TriggerVerifyAccount() {
<img <img
alt={'Please wait.'} alt={'Please wait.'}
src={SpinnerIcon} src={SpinnerIcon}
class={'mx-auto h-16 w-16 animate-spin'} className={'mx-auto h-16 w-16 animate-spin'}
/> />
)} )}
{error && ( {error && (
@ -75,7 +75,7 @@ export function TriggerVerifyAccount() {
</h2> </h2>
<div className="text-sm sm:text-base"> <div className="text-sm sm:text-base">
{isLoading && <p>Please wait while we verify your account..</p>} {isLoading && <p>Please wait while we verify your account..</p>}
{error && <p class="text-red-700">{error}</p>} {error && <p className="text-red-700">{error}</p>}
</div> </div>
</div> </div>
</div> </div>

@ -1,5 +1,5 @@
import VerifyLetterIcon from '../../icons/verify-letter.svg'; import VerifyLetterIcon from '../../icons/verify-letter.svg';
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
import { httpPost } from '../../lib/http'; import { httpPost } from '../../lib/http';
export function VerificationEmailMessage() { export function VerificationEmailMessage() {
@ -39,13 +39,13 @@ export function VerificationEmailMessage() {
<div className="mx-auto max-w-md text-center"> <div className="mx-auto max-w-md text-center">
<img <img
alt="Verify Email" alt="Verify Email"
src={VerifyLetterIcon} src={VerifyLetterIcon as any}
class="mx-auto mb-4 h-20 w-40 sm:h-40" className="mx-auto mb-4 h-20 w-40 sm:h-40"
/> />
<h2 class="my-2 text-center text-xl font-semibold sm:my-5 sm:text-2xl"> <h2 className="my-2 text-center text-xl font-semibold sm:my-5 sm:text-2xl">
Verify your email address Verify your email address
</h2> </h2>
<div class="text-sm sm:text-base"> <div className="text-sm sm:text-base">
<p> <p>
We have sent you an email at{' '} We have sent you an email at{' '}
<span className="font-bold">{email}</span>. Please click the link to <span className="font-bold">{email}</span>. Please click the link to
@ -53,7 +53,7 @@ export function VerificationEmailMessage() {
soon! soon!
</p> </p>
<hr class="my-4" /> <hr className="my-4" />
{!isEmailResent && ( {!isEmailResent && (
<> <>
@ -72,12 +72,12 @@ export function VerificationEmailMessage() {
</p> </p>
)} )}
{error && <p class="text-red-700">{error}</p>} {error && <p className="text-red-700">{error}</p>}
</> </>
)} )}
{isEmailResent && ( {isEmailResent && (
<p class="text-green-700">Verification email has been sent!</p> <p className="text-green-700">Verification email has been sent!</p>
)} )}
</div> </div>
</div> </div>

@ -1,5 +1,5 @@
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
import { httpDelete, httpGet, httpPatch, httpPost } from '../lib/http'; import { httpDelete, httpGet, httpPost } from '../lib/http';
import { pageProgressMessage } from '../stores/page'; import { pageProgressMessage } from '../stores/page';
import { isLoggedIn } from '../lib/jwt'; import { isLoggedIn } from '../lib/jwt';
import { showLoginPopup } from '../lib/popup'; import { showLoginPopup } from '../lib/popup';
@ -118,7 +118,7 @@ export function Befriend() {
<ErrorIcon additionalClasses="mx-auto mb-4 mt-24 w-20 opacity-20" /> <ErrorIcon additionalClasses="mx-auto mb-4 mt-24 w-20 opacity-20" />
<h2 className={'mb-1 text-2xl font-bold'}>Error</h2> <h2 className={'mb-1 text-2xl font-bold'}>Error</h2>
<p class="mb-4 text-base leading-6 text-gray-600"> <p className="mb-4 text-base leading-6 text-gray-600">
{error || 'There was a problem, please try again.'} {error || 'There was a problem, please try again.'}
</p> </p>
@ -149,13 +149,13 @@ export function Befriend() {
/> />
<h2 className={'mb-1 text-3xl font-bold'}>{user.name}</h2> <h2 className={'mb-1 text-3xl font-bold'}>{user.name}</h2>
<p class="mb-6 text-base leading-6 text-gray-600"> <p className="mb-6 text-base leading-6 text-gray-600">
After you add {user.name} as a friend, you will be able to view each After you add {user.name} as a friend, you will be able to view each
other's skills and progress. other's skills and progress.
</p> </p>
<div class="mx-auto w-full duration-500 sm:max-w-md"> <div className="mx-auto w-full duration-500 sm:max-w-md">
<div class="flex w-full flex-col items-center gap-2"> <div className="flex w-full flex-col items-center gap-2">
{user.status === 'none' && ( {user.status === 'none' && (
<button <button
disabled={isMe} disabled={isMe}
@ -169,7 +169,7 @@ export function Befriend() {
}); });
}} }}
type="button" type="button"
class="w-full flex-grow cursor-pointer rounded-lg bg-black px-3 py-2 text-center text-white disabled:cursor-not-allowed disabled:opacity-40" className="w-full flex-grow cursor-pointer rounded-lg bg-black px-3 py-2 text-center text-white disabled:cursor-not-allowed disabled:opacity-40"
> >
{isMe ? "You can't add yourself" : 'Add Friend'} {isMe ? "You can't add yourself" : 'Add Friend'}
</button> </button>
@ -177,7 +177,7 @@ export function Befriend() {
{user.status === 'sent' && ( {user.status === 'sent' && (
<> <>
<span class="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-gray-300 px-3 py-2 text-center text-black"> <span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-gray-300 px-3 py-2 text-center text-black">
<CheckIcon additionalClasses="mr-2 h-4 w-4" /> <CheckIcon additionalClasses="mr-2 h-4 w-4" />
Request Sent Request Sent
</span> </span>
@ -188,7 +188,7 @@ export function Befriend() {
setIsConfirming(true); setIsConfirming(true);
}} }}
type="button" type="button"
class="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-red-600 bg-red-600 px-3 py-2 text-center text-white hover:bg-red-700" className="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-red-600 bg-red-600 px-3 py-2 text-center text-white hover:bg-red-700"
> >
<DeleteUserIcon additionalClasses="mr-2 h-[19px] w-[19px]" /> <DeleteUserIcon additionalClasses="mr-2 h-[19px] w-[19px]" />
Withdraw Request Withdraw Request
@ -196,7 +196,7 @@ export function Befriend() {
)} )}
{isConfirming && ( {isConfirming && (
<span class="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600"> <span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600">
Are you sure?{' '} Are you sure?{' '}
<button <button
className="ml-2 text-red-700 underline" className="ml-2 text-red-700 underline"
@ -225,7 +225,7 @@ export function Befriend() {
{user.status === 'accepted' && ( {user.status === 'accepted' && (
<> <>
<span class="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-gray-300 px-3 py-2 text-center text-black"> <span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-gray-300 px-3 py-2 text-center text-black">
<AddedUserIcon additionalClasses="mr-2 h-5 w-5" /> <AddedUserIcon additionalClasses="mr-2 h-5 w-5" />
You are friends You are friends
</span> </span>
@ -236,7 +236,7 @@ export function Befriend() {
setIsConfirming(true); setIsConfirming(true);
}} }}
type="button" type="button"
class="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-red-600 bg-red-600 px-3 py-2 text-center text-white hover:bg-red-700" className="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-red-600 bg-red-600 px-3 py-2 text-center text-white hover:bg-red-700"
> >
<DeleteUserIcon additionalClasses="mr-2 h-[19px] w-[19px]" /> <DeleteUserIcon additionalClasses="mr-2 h-[19px] w-[19px]" />
Remove Friend Remove Friend
@ -244,7 +244,7 @@ export function Befriend() {
)} )}
{isConfirming && ( {isConfirming && (
<span class="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600"> <span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600">
Are you sure?{' '} Are you sure?{' '}
<button <button
className="ml-2 text-red-700 underline" className="ml-2 text-red-700 underline"
@ -271,12 +271,12 @@ export function Befriend() {
{user.status === 'rejected' && ( {user.status === 'rejected' && (
<> <>
<span class="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-gray-300 px-3 py-2 text-center text-black"> <span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-gray-300 px-3 py-2 text-center text-black">
<DeleteUserIcon additionalClasses="mr-2 h-4 w-4" /> <DeleteUserIcon additionalClasses="mr-2 h-4 w-4" />
Request Rejected Request Rejected
</span> </span>
<span class="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600"> <span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600">
Changed your mind?{' '} Changed your mind?{' '}
<button <button
className="ml-2 text-red-700 underline" className="ml-2 text-red-700 underline"
@ -296,7 +296,7 @@ export function Befriend() {
{user.status === 'got_rejected' && ( {user.status === 'got_rejected' && (
<> <>
<span class="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-500 px-3 py-2 text-center text-red-500"> <span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-500 px-3 py-2 text-center text-red-500">
<StopIcon additionalClasses="mr-2 h-4 w-4" /> <StopIcon additionalClasses="mr-2 h-4 w-4" />
Request Rejected Request Rejected
</span> </span>
@ -311,7 +311,7 @@ export function Befriend() {
pageProgressMessage.set(''); pageProgressMessage.set('');
}); });
}} }}
class="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-gray-800 bg-gray-800 px-3 py-2 text-center text-white hover:bg-black" className="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-gray-800 bg-gray-800 px-3 py-2 text-center text-white hover:bg-black"
> >
<CheckIcon additionalClasses="mr-2 h-4 w-4" /> <CheckIcon additionalClasses="mr-2 h-4 w-4" />
Accept Request Accept Request
@ -323,7 +323,7 @@ export function Befriend() {
setIsConfirming(true); setIsConfirming(true);
}} }}
type="button" type="button"
class="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-red-600 bg-white px-3 py-2 text-center text-red-600 hover:bg-red-100" className="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-red-600 bg-white px-3 py-2 text-center text-red-600 hover:bg-red-100"
> >
<DeleteUserIcon additionalClasses="mr-2 h-[19px] w-[19px]" /> <DeleteUserIcon additionalClasses="mr-2 h-[19px] w-[19px]" />
Reject Request Reject Request
@ -331,7 +331,7 @@ export function Befriend() {
)} )}
{isConfirming && ( {isConfirming && (
<span class="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600"> <span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600">
Are you sure?{' '} Are you sure?{' '}
<button <button
className="ml-2 text-red-700 underline" className="ml-2 text-red-700 underline"

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'preact/hooks'; import { Fragment, useEffect, useRef, useState } from 'react';
import { useKeydown } from '../../hooks/use-keydown'; import { useKeydown } from '../../hooks/use-keydown';
import { useOutsideClick } from '../../hooks/use-outside-click'; import { useOutsideClick } from '../../hooks/use-outside-click';
import BestPracticesIcon from '../../icons/best-practices.svg'; import BestPracticesIcon from '../../icons/best-practices.svg';
@ -158,12 +158,12 @@ export function CommandMenu() {
<div className="relative rounded-lg bg-white shadow" ref={modalRef}> <div className="relative rounded-lg bg-white shadow" ref={modalRef}>
<input <input
ref={inputRef} ref={inputRef}
autofocus={true} autoFocus={true}
type="text" type="text"
value={searchedText} value={searchedText}
className="w-full rounded-t-md border-b p-4 text-sm focus:bg-gray-50 focus:outline-none" className="w-full rounded-t-md border-b p-4 text-sm focus:bg-gray-50 focus:outline-none"
placeholder="Search roadmaps, guides or pages .." placeholder="Search roadmaps, guides or pages .."
autocomplete="off" autoComplete="off"
onInput={(e) => { onInput={(e) => {
const value = (e.target as HTMLInputElement).value.trim(); const value = (e.target as HTMLInputElement).value.trim();
setSearchedText(value); setSearchedText(value);
@ -191,39 +191,43 @@ export function CommandMenu() {
}} }}
/> />
<div class="px-2 py-2"> <div className="px-2 py-2">
<div className="flex flex-col"> <div className="flex flex-col">
{searchResults.length === 0 && ( {searchResults.length === 0 && (
<div class="p-5 text-center text-sm text-gray-400"> <div className="p-5 text-center text-sm text-gray-400">
No results found No results found
</div> </div>
)} )}
{searchResults.map((page, counter) => { {searchResults.map((page: PageType, counter: number) => {
const prevPage = searchResults[counter - 1]; const prevPage = searchResults[counter - 1];
const groupChanged = prevPage && prevPage.group !== page.group; const groupChanged = prevPage && prevPage.group !== page.group;
return ( return (
<> <Fragment key={page.id}>
{groupChanged && ( {groupChanged && (
<div class="border-b border-gray-100"></div> <div className="border-b border-gray-100"></div>
)} )}
<a <a
class={`flex w-full items-center rounded p-2 text-sm ${ className={`flex w-full items-center rounded p-2 text-sm ${
counter === activeCounter ? 'bg-gray-100' : '' counter === activeCounter ? 'bg-gray-100' : ''
}`} }`}
onMouseOver={() => setActiveCounter(counter)} onMouseOver={() => setActiveCounter(counter)}
href={page.url} href={page.url}
> >
{!page.icon && ( {!page.icon && (
<span class="mr-2 text-gray-400">{page.group}</span> <span className="mr-2 text-gray-400">{page.group}</span>
)} )}
{page.icon && ( {page.icon && (
<img alt={page.title} src={page.icon} class="mr-2 h-4 w-4" /> <img
alt={page.title}
src={page.icon as any}
className="mr-2 h-4 w-4"
/>
)} )}
{page.title} {page.title}
</a> </a>
</> </Fragment>
); );
})} })}
</div> </div>

@ -1,4 +1,4 @@
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
import { Stepper } from '../Stepper'; import { Stepper } from '../Stepper';
import { Step0, ValidTeamType } from './Step0'; import { Step0, ValidTeamType } from './Step0';
import { Step1, ValidTeamSize } from './Step1'; import { Step1, ValidTeamSize } from './Step1';

@ -1,4 +1,4 @@
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
import { httpGet, httpPut } from '../../lib/http'; import { httpGet, httpPut } from '../../lib/http';
import type { PageType } from '../CommandMenu/CommandMenu'; import type { PageType } from '../CommandMenu/CommandMenu';
import ChevronDownIcon from '../../icons/chevron-down.svg'; import ChevronDownIcon from '../../icons/chevron-down.svg';

@ -1,5 +1,5 @@
import { ChevronDownIcon } from '../ReactIcons/ChevronDownIcon'; import { ChevronDownIcon } from '../ReactIcons/ChevronDownIcon';
import { useRef, useState } from 'preact/hooks'; import { useRef, useState } from 'react';
import { useOutsideClick } from '../../hooks/use-outside-click'; import { useOutsideClick } from '../../hooks/use-outside-click';
const allowedRoles = [ const allowedRoles = [

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'react';
import { useKeydown } from '../../hooks/use-keydown'; import { useKeydown } from '../../hooks/use-keydown';
import { useOutsideClick } from '../../hooks/use-outside-click'; import { useOutsideClick } from '../../hooks/use-outside-click';
import type { PageType } from '../CommandMenu/CommandMenu'; import type { PageType } from '../CommandMenu/CommandMenu';
@ -68,11 +68,11 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
); );
return ( return (
<div class="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50"> <div className="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50">
<div class="relative mx-auto h-full w-full max-w-2xl p-4 md:h-auto"> <div className="relative mx-auto h-full w-full max-w-2xl p-4 md:h-auto">
<div <div
ref={popupBodyEl} ref={popupBodyEl}
class="popup-body relative mt-4 overflow-hidden rounded-lg bg-white shadow" className="popup-body relative mt-4 overflow-hidden rounded-lg bg-white shadow"
> >
<button <button
type="button" type="button"
@ -80,7 +80,7 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
onClick={onClose} onClick={onClose}
> >
<img alt={'close'} src={CloseIcon} className="h-4 w-4" /> <img alt={'close'} src={CloseIcon} className="h-4 w-4" />
<span class="sr-only">Close modal</span> <span className="sr-only">Close modal</span>
</button> </button>
<input <input
ref={searchInputEl} ref={searchInputEl}

@ -2,7 +2,7 @@ import BuildingIcon from '../../icons/building.svg';
import UsersIcon from '../../icons/users.svg'; import UsersIcon from '../../icons/users.svg';
import type { TeamDocument } from './CreateTeamForm'; import type { TeamDocument } from './CreateTeamForm';
import { httpPut } from '../../lib/http'; import { httpPut } from '../../lib/http';
import { useState } from 'preact/hooks'; import { useState } from 'react';
import { NextButton } from './NextButton'; import { NextButton } from './NextButton';
export const validTeamTypes = [ export const validTeamTypes = [

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'react';
import { AppError, httpPost, httpPut } from '../../lib/http'; import { AppError, httpPost, httpPut } from '../../lib/http';
import type { ValidTeamType } from './Step0'; import type { ValidTeamType } from './Step0';
import type { TeamDocument } from './CreateTeamForm'; import type { TeamDocument } from './CreateTeamForm';
@ -133,7 +133,7 @@ export function Step1(props: Step1Props) {
type="text" type="text"
name="name" name="name"
ref={nameRef as any} ref={nameRef as any}
autofocus={true} autoFocus={true}
id="name" id="name"
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 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"
placeholder="Roadmap Inc." placeholder="Roadmap Inc."

@ -2,7 +2,7 @@ import type { TeamDocument } from './CreateTeamForm';
import { NextButton } from './NextButton'; import { NextButton } from './NextButton';
import { TrashIcon } from '../ReactIcons/TrashIcon'; import { TrashIcon } from '../ReactIcons/TrashIcon';
import { AllowedRoles, RoleDropdown } from './RoleDropdown'; import { AllowedRoles, RoleDropdown } from './RoleDropdown';
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'react';
import { httpPost } from '../../lib/http'; import { httpPost } from '../../lib/http';
type Step3Props = { type Step3Props = {
@ -75,9 +75,9 @@ export function Step3(props: Step3Props) {
return ( return (
<form className="mt-4 flex w-full flex-col" onSubmit={onSubmit}> <form className="mt-4 flex w-full flex-col" onSubmit={onSubmit}>
<div class="mb-1 mt-2"> <div className="mb-1 mt-2">
<h2 class="mb-1 md:mb-2 text-lg md:text-2xl font-bold">Invite your Team</h2> <h2 className="mb-1 md:mb-2 text-lg md:text-2xl font-bold">Invite your Team</h2>
<p class="text-sm text-gray-700"> <p className="text-sm text-gray-700">
Use the form below to invite your team members to your team. You can Use the form below to invite your team members to your team. You can
also invite them later. also invite them later.
</p> </p>
@ -88,7 +88,7 @@ export function Step3(props: Step3Props) {
<div className="flex flex-col sm:flex-row gap-2" key={user.id}> <div className="flex flex-col sm:flex-row gap-2" key={user.id}>
<input <input
ref={userCounter === users.length - 1 ? emailInputRef : null} ref={userCounter === users.length - 1 ? emailInputRef : null}
autofocus={true} autoFocus={true}
type="email" type="email"
name="email" name="email"
required required

@ -8,15 +8,15 @@ type Step4Props = {
export function Step4({ team }: Step4Props) { export function Step4({ team }: Step4Props) {
return ( return (
<div className="mt-4 flex flex-col rounded-xl border py-12 text-center"> <div className="mt-4 flex flex-col rounded-xl border py-12 text-center">
<div class="mb-1 flex flex-col items-center"> <div className="mb-1 flex flex-col items-center">
<CheckIcon additionalClasses={'h-14 w-14 mb-4 opacity-100'} /> <CheckIcon additionalClasses={'h-14 w-14 mb-4 opacity-100'} />
<h2 class="mb-2 text-2xl font-bold">Team Created</h2> <h2 className="mb-2 text-2xl font-bold">Team Created</h2>
<p class="text-sm text-gray-700"> <p className="text-sm text-gray-700">
Your team has been created. Happy learning! Your team has been created. Happy learning!
</p> </p>
<a <a
href={`/team/progress?t=${team._id}`} href={`/team/progress?t=${team._id}`}
class="mt-4 rounded-md bg-black px-5 py-1.5 text-sm text-white" className="mt-4 rounded-md bg-black px-5 py-1.5 text-sm text-white"
> >
View Team View Team
</a> </a>

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'react';
import { wireframeJSONToSVG } from 'roadmap-renderer'; import { wireframeJSONToSVG } from 'roadmap-renderer';
import { Spinner } from '../ReactIcons/Spinner'; import { Spinner } from '../ReactIcons/Spinner';
import { httpGet, httpPut } from '../../lib/http'; import { httpGet, httpPut } from '../../lib/http';
@ -8,7 +8,7 @@ import { useOutsideClick } from '../../hooks/use-outside-click';
import { useKeydown } from '../../hooks/use-keydown'; import { useKeydown } from '../../hooks/use-keydown';
import type { TeamResourceConfig } from './RoadmapSelector'; import type { TeamResourceConfig } from './RoadmapSelector';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import { useStore } from '@nanostores/preact'; import { useStore } from '@nanostores/react';
import { $currentTeam } from '../../stores/team'; import { $currentTeam } from '../../stores/team';
export type ProgressMapProps = { export type ProgressMapProps = {
@ -148,8 +148,8 @@ export function UpdateTeamResourceModal(props: ProgressMapProps) {
}, []); }, []);
return ( return (
<div class="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50"> <div className="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50">
<div class="relative mx-auto h-full w-full max-w-4xl p-4 md:h-auto"> <div className="relative mx-auto h-full w-full max-w-4xl p-4 md:h-auto">
<div <div
id={ id={
currentTeam?.type === 'company' currentTeam?.type === 'company'
@ -157,7 +157,7 @@ export function UpdateTeamResourceModal(props: ProgressMapProps) {
: 'original-roadmap' : 'original-roadmap'
} }
ref={popupBodyEl} ref={popupBodyEl}
class="popup-body relative rounded-lg bg-white shadow" className="popup-body relative rounded-lg bg-white shadow"
> >
<div <div
className={ className={
@ -201,7 +201,7 @@ export function UpdateTeamResourceModal(props: ProgressMapProps) {
<div id="resource-svg-wrap" ref={containerEl} className="px-4"></div> <div id="resource-svg-wrap" ref={containerEl} className="px-4"></div>
{isLoading && ( {isLoading && (
<div class="flex w-full justify-center"> <div className="flex w-full justify-center">
<Spinner <Spinner
isDualRing={false} isDualRing={false}
className="mb-4 mt-2 h-4 w-4 animate-spin fill-blue-600 text-gray-200 sm:h-8 sm:w-8" className="mb-4 mt-2 h-4 w-4 animate-spin fill-blue-600 text-gray-200 sm:h-8 sm:w-8"

@ -1,4 +1,4 @@
import {useEffect, useState} from 'preact/hooks'; import {useEffect, useState} from 'react';
import { httpDelete } from '../../lib/http'; import { httpDelete } from '../../lib/http';
import { logout } from '../Navigation/navigation'; import { logout } from '../Navigation/navigation';

@ -12,6 +12,6 @@ import { DeleteAccountForm } from './DeleteAccountForm';
<p class="text-black font-medium -mb-2 mt-3 text-base">Please type "delete" to confirm.</p> <p class="text-black font-medium -mb-2 mt-3 text-base">Please type "delete" to confirm.</p>
<DeleteAccountForm client:only /> <DeleteAccountForm client:only="react" />
</div> </div>
</Popup> </Popup>

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'react';
import { httpDelete } from '../lib/http'; import { httpDelete } from '../lib/http';
import type { TeamDocument } from './CreateTeam/CreateTeamForm'; import type { TeamDocument } from './CreateTeam/CreateTeamForm';
import { useTeamId } from '../hooks/use-team-id'; import { useTeamId } from '../hooks/use-team-id';
@ -69,18 +69,18 @@ export function DeleteTeamPopup(props: DeleteTeamPopupProps) {
return ( return (
<> <>
<div class="fixed left-0 right-0 top-0 z-50 flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50"> <div className="fixed left-0 right-0 top-0 z-50 flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50">
<div class="relative h-full w-full max-w-md p-4 md:h-auto"> <div className="relative h-full w-full max-w-md p-4 md:h-auto">
<div <div
ref={popupBodyEl} ref={popupBodyEl}
class="popup-body relative rounded-lg bg-white p-4 shadow" className="popup-body relative rounded-lg bg-white p-4 shadow"
> >
<h2 class="text-2xl font-semibold text-black">Delete Team</h2> <h2 className="text-2xl font-semibold text-black">Delete Team</h2>
<p className="text-gray-500"> <p className="text-gray-500">
This will permanently delete your team and all associated data. This will permanently delete your team and all associated data.
</p> </p>
<p class="-mb-2 mt-3 text-base font-medium text-black"> <p className="-mb-2 mt-3 text-base font-medium text-black">
Please type "delete" to confirm. Please type "delete" to confirm.
</p> </p>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>

@ -16,8 +16,8 @@ export function FavoriteIcon(props: FavoriteIconProps) {
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
fill-rule="evenodd" fillRule="evenodd"
clip-rule="evenodd" clipRule="evenodd"
d="M5.93682 0.5H2.06282C1.63546 0.500094 1.22423 0.663195 0.912987 0.956045C0.601741 1.2489 0.413919 1.64944 0.387822 2.076L0.00182198 8.461C-0.012178 8.6905 0.0548218 8.9185 0.191822 9.104L0.242322 9.1665C0.575322 9.5485 1.15132 9.6165 1.56582 9.31L3.99982 7.5115L6.43382 9.31C6.58413 9.42115 6.76305 9.48708 6.94954 9.50006C7.13603 9.51303 7.32235 9.4725 7.4866 9.38323C7.65085 9.29397 7.78621 9.15967 7.87677 8.99613C7.96733 8.83258 8.00932 8.64659 7.99782 8.46L7.61232 2.0765C7.58622 1.64981 7.39835 1.24914 7.08701 0.956192C6.77567 0.663248 6.36431 0.500094 5.93682 0.5ZM5.93682 1.25C6.42732 1.25 6.83382 1.632 6.86382 2.122L7.24932 8.506C7.25216 8.55018 7.24229 8.59425 7.22089 8.63301C7.19949 8.67176 7.16745 8.70359 7.12854 8.72472C7.08964 8.74585 7.0455 8.75542 7.00134 8.75228C6.95718 8.74914 6.91484 8.73343 6.87932 8.707L4.27582 6.783C4.19591 6.72397 4.09917 6.69211 3.99982 6.69211C3.90047 6.69211 3.80373 6.72397 3.72382 6.783L1.11982 8.707C1.0843 8.73343 1.04196 8.74914 0.9978 8.75228C0.953639 8.75542 0.909502 8.74585 0.8706 8.72472C0.831697 8.70359 0.799653 8.67176 0.778252 8.63301C0.756851 8.59425 0.746986 8.55018 0.749822 8.506L1.13632 2.122C1.16632 1.632 1.57232 1.25 2.06282 1.25H5.93682Z" d="M5.93682 0.5H2.06282C1.63546 0.500094 1.22423 0.663195 0.912987 0.956045C0.601741 1.2489 0.413919 1.64944 0.387822 2.076L0.00182198 8.461C-0.012178 8.6905 0.0548218 8.9185 0.191822 9.104L0.242322 9.1665C0.575322 9.5485 1.15132 9.6165 1.56582 9.31L3.99982 7.5115L6.43382 9.31C6.58413 9.42115 6.76305 9.48708 6.94954 9.50006C7.13603 9.51303 7.32235 9.4725 7.4866 9.38323C7.65085 9.29397 7.78621 9.15967 7.87677 8.99613C7.96733 8.83258 8.00932 8.64659 7.99782 8.46L7.61232 2.0765C7.58622 1.64981 7.39835 1.24914 7.08701 0.956192C6.77567 0.663248 6.36431 0.500094 5.93682 0.5ZM5.93682 1.25C6.42732 1.25 6.83382 1.632 6.86382 2.122L7.24932 8.506C7.25216 8.55018 7.24229 8.59425 7.22089 8.63301C7.19949 8.67176 7.16745 8.70359 7.12854 8.72472C7.08964 8.74585 7.0455 8.75542 7.00134 8.75228C6.95718 8.74914 6.91484 8.73343 6.87932 8.707L4.27582 6.783C4.19591 6.72397 4.09917 6.69211 3.99982 6.69211C3.90047 6.69211 3.80373 6.72397 3.72382 6.783L1.11982 8.707C1.0843 8.73343 1.04196 8.74914 0.9978 8.75228C0.953639 8.75542 0.909502 8.74585 0.8706 8.72472C0.831697 8.70359 0.799653 8.67176 0.778252 8.63301C0.756851 8.59425 0.746986 8.55018 0.749822 8.506L1.13632 2.122C1.16632 1.632 1.57232 1.25 2.06282 1.25H5.93682Z"
fill="currentColor" fill="currentColor"
/> />
@ -35,8 +35,8 @@ export function FavoriteIcon(props: FavoriteIconProps) {
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
fill-rule="evenodd" fillRule="evenodd"
clip-rule="evenodd" clipRule="evenodd"
d="M5.93682 0.5H2.06282C1.63546 0.500094 1.22423 0.663195 0.912987 0.956045C0.601741 1.2489 0.413919 1.64944 0.387822 2.076L0.00182198 8.461C-0.012178 8.6905 0.0548218 8.9185 0.191822 9.104L0.242322 9.1665C0.575322 9.5485 1.15132 9.6165 1.56582 9.31L3.99982 7.5115L6.43382 9.31C6.58413 9.42115 6.76305 9.48708 6.94954 9.50006C7.13603 9.51303 7.32235 9.4725 7.4866 9.38323C7.65085 9.29397 7.78621 9.15967 7.87677 8.99613C7.96733 8.83258 8.00932 8.64659 7.99782 8.46L7.61232 2.0765C7.58622 1.64981 7.39835 1.24914 7.08701 0.956192C6.77567 0.663248 6.36431 0.500094 5.93682 0.5Z" d="M5.93682 0.5H2.06282C1.63546 0.500094 1.22423 0.663195 0.912987 0.956045C0.601741 1.2489 0.413919 1.64944 0.387822 2.076L0.00182198 8.461C-0.012178 8.6905 0.0548218 8.9185 0.191822 9.104L0.242322 9.1665C0.575322 9.5485 1.15132 9.6165 1.56582 9.31L3.99982 7.5115L6.43382 9.31C6.58413 9.42115 6.76305 9.48708 6.94954 9.50006C7.13603 9.51303 7.32235 9.4725 7.4866 9.38323C7.65085 9.29397 7.78621 9.15967 7.87677 8.99613C7.96733 8.83258 8.00932 8.64659 7.99782 8.46L7.61232 2.0765C7.58622 1.64981 7.39835 1.24914 7.08701 0.956192C6.77567 0.663248 6.36431 0.500094 5.93682 0.5Z"
fill="currentColor" fill="currentColor"
/> />

@ -29,7 +29,7 @@ const { isUpcoming = false, isNew = false, text, url } = Astro.props;
<MarkFavorite <MarkFavorite
resourceId={url.split('/').pop()!} resourceId={url.split('/').pop()!}
resourceType={url.includes('best-practices') ? 'best-practice' : 'roadmap'} resourceType={url.includes('best-practices') ? 'best-practice' : 'roadmap'}
client:load client:only="react"
/> />
{ {

@ -1,4 +1,5 @@
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
import type { MouseEvent } from "react";
import { httpPatch } from '../../lib/http'; import { httpPatch } from '../../lib/http';
import type { ResourceType } from '../../lib/resource-progress'; import type { ResourceType } from '../../lib/resource-progress';
import { isLoggedIn } from '../../lib/jwt'; import { isLoggedIn } from '../../lib/jwt';
@ -20,15 +21,16 @@ export function MarkFavorite({
favorite, favorite,
className, className,
}: MarkFavoriteType) { }: MarkFavoriteType) {
const isAuthenticated = isLoggedIn();
const localStorageKey = `${resourceType}-${resourceId}-favorite`; const localStorageKey = `${resourceType}-${resourceId}-favorite`;
const toast = useToast(); const toast = useToast();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isFavorite, setIsFavorite] = useState( const [isFavorite, setIsFavorite] = useState(
favorite ?? localStorage.getItem(localStorageKey) === '1' isAuthenticated ? (favorite ?? localStorage.getItem(localStorageKey) === '1') : false
); );
async function toggleFavoriteHandler(e: Event) { async function toggleFavoriteHandler(e: MouseEvent<HTMLButtonElement>) {
e.preventDefault(); e.preventDefault();
if (!isLoggedIn()) { if (!isLoggedIn()) {
showLoginPopup(); showLoginPopup();

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'preact/hooks'; import { FormEvent, useEffect, useRef, useState } from 'react';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import { useTeamId } from '../../hooks/use-team-id'; import { useTeamId } from '../../hooks/use-team-id';
import { useOutsideClick } from '../../hooks/use-outside-click'; import { useOutsideClick } from '../../hooks/use-outside-click';
@ -13,7 +13,6 @@ type SubmitFeedbackPopupProps = {
export function SubmitFeedbackPopup(props: SubmitFeedbackPopupProps) { export function SubmitFeedbackPopup(props: SubmitFeedbackPopupProps) {
const { onClose } = props; const { onClose } = props;
const toast = useToast();
const popupBodyEl = useRef<HTMLDivElement>(null); const popupBodyEl = useRef<HTMLDivElement>(null);
const inputEl = useRef<HTMLTextAreaElement>(null); const inputEl = useRef<HTMLTextAreaElement>(null);
@ -35,7 +34,7 @@ export function SubmitFeedbackPopup(props: SubmitFeedbackPopupProps) {
inputEl.current?.focus(); inputEl.current?.focus();
}, []); }, []);
const handleSubmit = async (e: Event) => { const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
setIsLoading(true); setIsLoading(true);
setError(''); setError('');
@ -65,25 +64,27 @@ export function SubmitFeedbackPopup(props: SubmitFeedbackPopupProps) {
}; };
return ( return (
<div class="fixed left-0 right-0 top-0 z-50 flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50"> <div className="fixed left-0 right-0 top-0 z-50 flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50">
<div class="relative h-full w-full max-w-md p-4 md:h-auto"> <div className="relative h-full w-full max-w-md p-4 md:h-auto">
<div <div
ref={popupBodyEl} ref={popupBodyEl}
class="popup-body relative rounded-lg bg-white p-4 shadow" className="popup-body relative rounded-lg bg-white p-4 shadow"
> >
{!isSuccess && ( {!isSuccess && (
<> <>
<h2 class="text-xl font-semibold mb-1 text-black"> <h2 className="mb-1 text-xl font-semibold text-black">
Enter your feedback Enter your feedback
</h2> </h2>
<p className={'text-sm text-gray-500'}>Help us improve your experience.</p> <p className={'text-sm text-gray-500'}>
Help us improve your experience.
</p>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="my-4"> <div className="my-4">
<textarea <textarea
ref={inputEl} ref={inputEl}
name="submit-feedback" name="submit-feedback"
id="submit-feedback" id="submit-feedback"
className="mt-2 block min-h-[150px] w-full rounded-md border border-gray-300 px-3 py-2 outline-none placeholder:text-gray-400 resize-none" className="mt-2 block min-h-[150px] w-full resize-none rounded-md border border-gray-300 px-3 py-2 outline-none placeholder:text-gray-400"
placeholder="Enter your feedback" placeholder="Enter your feedback"
required required
autoFocus autoFocus

@ -11,19 +11,19 @@ export function EmptyFriends(props: EmptyFriendsProps) {
const { isCopied, copyText } = useCopyText(); const { isCopied, copyText } = useCopyText();
return ( return (
<div class="rounded-md"> <div className="rounded-md">
<div class="mx-auto flex flex-col items-center p-7 text-center"> <div className="mx-auto flex flex-col items-center p-7 text-center">
<img <img
alt="no friends" alt="no friends"
src={UserPlusIcon} src={UserPlusIcon as any}
class="mb-2 h-[60px] w-[60px] opacity-10 sm:h-[120px] sm:w-[120px]" className="mb-2 h-[60px] w-[60px] opacity-10 sm:h-[120px] sm:w-[120px]"
/> />
<h2 class="text-lg font-bold sm:text-xl">Invite your Friends</h2> <h2 className="text-lg font-bold sm:text-xl">Invite your Friends</h2>
<p className="mb-4 mt-1 max-w-[400px] text-sm leading-relaxed text-gray-500"> <p className="mb-4 mt-1 max-w-[400px] text-sm leading-relaxed text-gray-500">
Share the unique link below with your friends to track their skills and progress. Share the unique link below with your friends to track their skills and progress.
</p> </p>
<div class="flex w-full max-w-[352px] items-center justify-center gap-2 rounded-lg border-2 p-1 text-sm"> <div className="flex w-full max-w-[352px] items-center justify-center gap-2 rounded-lg border-2 p-1 text-sm">
<input <input
onClick={(e) => { onClick={(e) => {
e.currentTarget.select(); e.currentTarget.select();
@ -31,11 +31,11 @@ export function EmptyFriends(props: EmptyFriendsProps) {
}} }}
type="text" type="text"
value={befriendUrl} value={befriendUrl}
class="w-full border-none bg-transparent px-1.5 outline-none" className="w-full border-none bg-transparent px-1.5 outline-none"
readonly readOnly
/> />
<button <button
class={`flex items-center justify-center gap-1 rounded-md border-0 p-2 px-4 text-sm text-black ${ className={`flex items-center justify-center gap-1 rounded-md border-0 p-2 px-4 text-sm text-black ${
isCopied isCopied
? 'bg-green-300 hover:bg-green-300' ? 'bg-green-300 hover:bg-green-300'
: 'bg-gray-200 hover:bg-gray-300' : 'bg-gray-200 hover:bg-gray-300'
@ -44,7 +44,7 @@ export function EmptyFriends(props: EmptyFriendsProps) {
copyText(befriendUrl); copyText(befriendUrl);
}} }}
> >
<img src={CopyIcon} className="h-4 w-4" alt="Invite Friends" /> <img src={CopyIcon as any} className="h-4 w-4" alt="Invite Friends" />
{isCopied ? 'Copied' : 'Copy'} {isCopied ? 'Copied' : 'Copy'}
</button> </button>
</div> </div>

@ -1,4 +1,4 @@
import { useState } from 'preact/hooks'; import { useState } from 'react';
import type { ListFriendsResponse } from './FriendsPage'; import type { ListFriendsResponse } from './FriendsPage';
import { DeleteUserIcon } from '../ReactIcons/DeleteUserIcon'; import { DeleteUserIcon } from '../ReactIcons/DeleteUserIcon';
import { pageProgressMessage } from '../../stores/page'; import { pageProgressMessage } from '../../stores/page';
@ -177,12 +177,12 @@ export function FriendProgressItem(props: FriendProgressItemProps) {
<div <div
className={'flex w-full flex-grow items-center justify-center'} className={'flex w-full flex-grow items-center justify-center'}
> >
<span class=" flex flex-col items-center text-red-500"> <span className=" flex flex-col items-center text-red-500">
<DeleteUserIcon additionalClasses="mr-2 h-8 w-8 mb-1" /> <DeleteUserIcon additionalClasses="mr-2 h-8 w-8 mb-1" />
Request Rejected Request Rejected
</span> </span>
</div> </div>
<span class="flex cursor-default items-center justify-center border-t py-2 text-center text-sm"> <span className="flex cursor-default items-center justify-center border-t py-2 text-center text-sm">
Changed your mind?{' '} Changed your mind?{' '}
<button <button
className="ml-2 font-medium text-red-700 underline underline-offset-2 hover:text-red-500" className="ml-2 font-medium text-red-700 underline underline-offset-2 hover:text-red-500"
@ -205,12 +205,12 @@ export function FriendProgressItem(props: FriendProgressItemProps) {
<div <div
className={'flex w-full flex-grow items-center justify-center'} className={'flex w-full flex-grow items-center justify-center'}
> >
<span class=" flex flex-col items-center text-sm text-red-500"> <span className=" flex flex-col items-center text-sm text-red-500">
<DeleteUserIcon additionalClasses="mr-2 h-8 w-8 mb-1" /> <DeleteUserIcon additionalClasses="mr-2 h-8 w-8 mb-1" />
Request Rejected Request Rejected
</span> </span>
</div> </div>
<span class="flex cursor-default items-center justify-center border-t py-2.5 text-center text-sm"> <span className="flex cursor-default items-center justify-center border-t py-2.5 text-center text-sm">
<button <button
className="ml-2 flex items-center font-medium text-red-700 underline underline-offset-2 hover:text-red-500" className="ml-2 flex items-center font-medium text-red-700 underline underline-offset-2 hover:text-red-500"
onClick={() => { onClick={() => {
@ -233,7 +233,7 @@ export function FriendProgressItem(props: FriendProgressItemProps) {
<div <div
className={'flex w-full flex-grow items-center justify-center'} className={'flex w-full flex-grow items-center justify-center'}
> >
<span class=" flex flex-col items-center text-green-500"> <span className=" flex flex-col items-center text-green-500">
<AddedUserIcon additionalClasses="mr-2 h-8 w-8 mb-1" /> <AddedUserIcon additionalClasses="mr-2 h-8 w-8 mb-1" />
Request Sent Request Sent
</span> </span>

@ -1,4 +1,4 @@
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
import { pageProgressMessage } from '../../stores/page'; import { pageProgressMessage } from '../../stores/page';
import { useAuth } from '../../hooks/use-auth'; import { useAuth } from '../../hooks/use-auth';
import { AddUserIcon } from '../ReactIcons/AddUserIcon'; import { AddUserIcon } from '../ReactIcons/AddUserIcon';
@ -133,6 +133,7 @@ export function FriendsPage() {
return ( return (
<button <button
key={grouping.value}
className={`relative flex items-center justify-center rounded-md border p-1 px-3 text-sm ${ className={`relative flex items-center justify-center rounded-md border p-1 px-3 text-sm ${
selectedGrouping === grouping.value selectedGrouping === grouping.value
? ' border-gray-400 bg-gray-200 ' ? ' border-gray-400 bg-gray-200 '
@ -154,7 +155,7 @@ export function FriendsPage() {
onClick={() => { onClick={() => {
setShowInviteFriendPopup(true); setShowInviteFriendPopup(true);
}} }}
class="flex items-center justify-center gap-1.5 rounded-md border border-gray-400 bg-gray-50 p-1 px-2 text-sm hover:border-gray-500 hover:bg-gray-100" className="flex items-center justify-center gap-1.5 rounded-md border border-gray-400 bg-gray-50 p-1 px-2 text-sm hover:border-gray-500 hover:bg-gray-100"
> >
<AddUserIcon additionalClasses="w-4 h-4" /> <AddUserIcon additionalClasses="w-4 h-4" />
Invite Friends Invite Friends

@ -1,4 +1,4 @@
import { useEffect, useRef } from 'preact/hooks'; import { useEffect, useRef } from 'react';
import { useOutsideClick } from '../../hooks/use-outside-click'; import { useOutsideClick } from '../../hooks/use-outside-click';
import CopyIcon from '../../icons/copy.svg'; import CopyIcon from '../../icons/copy.svg';
import { useCopyText } from '../../hooks/use-copy-text'; import { useCopyText } from '../../hooks/use-copy-text';
@ -22,13 +22,13 @@ export function InviteFriendPopup(props: InviteFriendPopupProps) {
useOutsideClick(popupBodyRef, handleClosePopup); useOutsideClick(popupBodyRef, handleClosePopup);
return ( return (
<div class="popup fixed left-0 right-0 top-0 z-50 flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50"> <div className="popup fixed left-0 right-0 top-0 z-50 flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50">
<div class="relative h-full w-full max-w-md p-4 md:h-auto"> <div className="relative h-full w-full max-w-md p-4 md:h-auto">
<div <div
ref={popupBodyRef} ref={popupBodyRef}
class="popup-body relative rounded-lg bg-white p-4 shadow" className="popup-body relative rounded-lg bg-white p-4 shadow"
> >
<h3 class="mb-1.5 text-xl font-medium sm:text-2xl">Invite URL</h3> <h3 className="mb-1.5 text-xl font-medium sm:text-2xl">Invite URL</h3>
<p className="mb-3 hidden text-sm leading-none text-gray-400 sm:block"> <p className="mb-3 hidden text-sm leading-none text-gray-400 sm:block">
Share the link below with your friends to invite them. Share the link below with your friends to invite them.
</p> </p>
@ -44,7 +44,7 @@ export function InviteFriendPopup(props: InviteFriendPopupProps) {
}} }}
/> />
<button <button
class={`flex items-center justify-center gap-1 rounded-md border-0 px-3 py-2.5 text-sm text-black ${ className={`flex items-center justify-center gap-1 rounded-md border-0 px-3 py-2.5 text-sm text-black ${
isCopied isCopied
? 'bg-green-300 hover:bg-green-300' ? 'bg-green-300 hover:bg-green-300'
: 'bg-gray-200 hover:bg-gray-300' : 'bg-gray-200 hover:bg-gray-300'

@ -1,6 +1,6 @@
import { httpGet } from '../../lib/http'; import { httpGet } from '../../lib/http';
import type { TeamListResponse } from '../TeamDropdown/TeamDropdown'; import type { TeamListResponse } from '../TeamDropdown/TeamDropdown';
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
type GetFriendCountsResponse = { type GetFriendCountsResponse = {
sentCount: number; sentCount: number;
@ -31,15 +31,15 @@ export function SidebarFriendsCounter() {
const pendingCount = friendCounts?.receivedCount || 0; const pendingCount = friendCounts?.receivedCount || 0;
if (!pendingCount) { if (!pendingCount) {
return ( return (
<span class="relative mr-1 flex items-center"> <span className="relative mr-1 flex items-center">
<span class="relative rounded-full bg-gray-200 p-1 text-xs" /> <span className="relative rounded-full bg-gray-200 p-1 text-xs" />
<span class="absolute bottom-0 left-0 right-0 top-0 animate-ping rounded-full bg-gray-400 p-1 text-xs" /> <span className="absolute bottom-0 left-0 right-0 top-0 animate-ping rounded-full bg-gray-400 p-1 text-xs" />
</span> </span>
); );
} }
return ( return (
<span class="flex h-4 w-4 items-center justify-center rounded-full bg-red-500 text-xs font-medium text-white"> <span className="flex h-4 w-4 items-center justify-center rounded-full bg-red-500 text-xs font-medium text-white">
{pendingCount} {pendingCount}
</span> </span>
); );

@ -1,7 +1,8 @@
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
import { EmptyProgress } from './EmptyProgress'; import { EmptyProgress } from './EmptyProgress';
import { httpGet } from '../../lib/http'; import { httpGet } from '../../lib/http';
import { ProgressList } from './ProgressList'; import { ProgressList } from './ProgressList';
import {isLoggedIn} from "../../lib/jwt";
export type UserProgressResponse = { export type UserProgressResponse = {
resourceId: string; resourceId: string;
@ -48,6 +49,11 @@ function renderProgress(progressList: UserProgressResponse) {
} }
export function FavoriteRoadmaps() { export function FavoriteRoadmaps() {
const isAuthenticated = isLoggedIn();
if (!isAuthenticated) {
return null;
}
const [isPreparing, setIsPreparing] = useState(true); const [isPreparing, setIsPreparing] = useState(true);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [progress, setProgress] = useState<UserProgressResponse>([]); const [progress, setProgress] = useState<UserProgressResponse>([]);
@ -109,7 +115,7 @@ export function FavoriteRoadmaps() {
return ( return (
<div <div
class={`flex min-h-[192px] bg-gradient-to-b transition-opacity duration-500 sm:min-h-[280px] opacity-${containerOpacity} ${ className={`flex min-h-[192px] bg-gradient-to-b transition-opacity duration-500 sm:min-h-[280px] opacity-${containerOpacity} ${
hasProgress && `border-t border-t-[#1e293c]` hasProgress && `border-t border-t-[#1e293c]`
}`} }`}
> >

@ -24,5 +24,5 @@ import { FavoriteRoadmaps } from './FavoriteRoadmaps';
their career. their career.
</p> </p>
</div> </div>
<FavoriteRoadmaps client:authenticated /> <FavoriteRoadmaps client:only="react" />
</div> </div>

@ -44,7 +44,7 @@ export function ProgressList(props: ProgressListProps) {
<span className="relative z-20">{resource.resourceTitle}</span> <span className="relative z-20">{resource.resourceTitle}</span>
<span <span
class="absolute bottom-0 left-0 top-0 z-10 bg-[#172a3a]" className="absolute bottom-0 left-0 top-0 z-10 bg-[#172a3a]"
style={{ width: `${percentageDone}%` }} style={{ width: `${percentageDone}%` }}
></span> ></span>
<MarkFavorite <MarkFavorite

@ -1,4 +1,4 @@
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
import { httpGet, httpPatch, httpPost } from '../../lib/http'; import { httpGet, httpPatch, httpPost } from '../../lib/http';
import { pageProgressMessage } from '../../stores/page'; import { pageProgressMessage } from '../../stores/page';
import type { TeamMemberDocument } from '../TeamMembers/TeamMembersPage'; import type { TeamMemberDocument } from '../TeamMembers/TeamMembersPage';
@ -62,7 +62,7 @@ export function NotificationPage() {
return ( return (
<div> <div>
<div class="mb-8 hidden md:block"> <div className="mb-8 hidden md:block">
<h2 className="text-3xl font-bold sm:text-4xl">Notification</h2> <h2 className="text-3xl font-bold sm:text-4xl">Notification</h2>
<p className="mt-2 text-gray-400">Manage your notifications</p> <p className="mt-2 text-gray-400">Manage your notifications</p>
</div> </div>

@ -1,7 +1,7 @@
import { useStore } from '@nanostores/preact'; import { useStore } from '@nanostores/react';
import SpinnerIcon from '../icons/spinner.svg'; import SpinnerIcon from '../icons/spinner.svg';
import { pageProgressMessage } from '../stores/page'; import { pageProgressMessage } from '../stores/page';
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
export interface Props { export interface Props {
initialMessage: string; initialMessage: string;
@ -29,9 +29,9 @@ export function PageProgress(props: Props) {
<div> <div>
{/* Tailwind based spinner for full page */} {/* Tailwind based spinner for full page */}
<div className="fixed left-0 top-0 z-50 flex h-full w-full items-center justify-center bg-white bg-opacity-75"> <div className="fixed left-0 top-0 z-50 flex h-full w-full items-center justify-center bg-white bg-opacity-75">
<div class="flex items-center justify-center rounded-md border bg-white px-4 py-2 "> <div className="flex items-center justify-center rounded-md border bg-white px-4 py-2 ">
<img <img
src={SpinnerIcon} src={SpinnerIcon as any}
alt="Loading" alt="Loading"
className="h-4 w-4 animate-spin fill-blue-600 text-gray-200 sm:h-4 sm:w-4" className="h-4 w-4 animate-spin fill-blue-600 text-gray-200 sm:h-4 sm:w-4"
/> />

@ -1,8 +1,8 @@
import { useStore } from '@nanostores/preact'; import { useEffect, useState } from 'react';
import { useEffect, useState } from 'preact/hooks';
import CloseIcon from '../icons/close.svg'; import CloseIcon from '../icons/close.svg';
import { httpGet } from '../lib/http'; import { httpGet } from '../lib/http';
import { sponsorHidden } from '../stores/page'; import { sponsorHidden } from '../stores/page';
import { useStore } from '@nanostores/react';
export type PageSponsorType = { export type PageSponsorType = {
company: string; company: string;
@ -77,15 +77,14 @@ export function PageSponsor(props: PageSponsorProps) {
return null; return null;
} }
const { url, title, imageUrl, description, company, gaLabel, pageUrl } = const { url, title, imageUrl, description, company, gaLabel } = sponsor;
sponsor;
return ( return (
<a <a
href={url} href={url}
target="_blank" target="_blank"
rel="noopener sponsored nofollow" rel="noopener sponsored nofollow"
class="fixed bottom-[15px] right-[15px] z-50 flex max-w-[350px] bg-white shadow-lg outline-0 outline-transparent" className="fixed bottom-[15px] right-[15px] z-50 flex max-w-[350px] bg-white shadow-lg outline-0 outline-transparent"
onClick={() => { onClick={() => {
window.fireEvent({ window.fireEvent({
category: 'SponsorClick', category: 'SponsorClick',
@ -95,28 +94,26 @@ export function PageSponsor(props: PageSponsorProps) {
}} }}
> >
<span <span
class="absolute right-1.5 top-1.5 text-gray-300 hover:text-gray-800" className="absolute right-1.5 top-1.5 text-gray-300 hover:text-gray-800"
aria-label="Close" aria-label="Close"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation();
sponsorHidden.set(true); sponsorHidden.set(true);
}} }}
> >
<img alt="Close" class="h-4 w-4" src={CloseIcon} /> <img alt="Close" className="h-4 w-4" src={CloseIcon as any} />
</span> </span>
<img <img
src={imageUrl} src={imageUrl}
class="block h-[150px] w-[104.89px] object-contain lg:h-[169px] lg:w-[118.18px]" className="block h-[150px] w-[104.89px] object-contain lg:h-[169px] lg:w-[118.18px]"
alt="Sponsor Banner" alt="Sponsor Banner"
/> />
<span class="flex flex-1 flex-col justify-between text-sm"> <span className="flex flex-1 flex-col justify-between text-sm">
<span class="p-[10px]"> <span className="p-[10px]">
<span class="mb-0.5 block font-semibold">{title}</span> <span className="mb-0.5 block font-semibold">{title}</span>
<span class="block text-gray-500">{description}</span> <span className="block text-gray-500">{description}</span>
</span> </span>
<span class="sponsor-footer">Partner Content</span> <span className="sponsor-footer">Partner Content</span>
</span> </span>
</a> </a>
); );

@ -13,9 +13,9 @@ export function AddUserIcon(props: CheckIconProps) {
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-width="2" strokeWidth="2"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" strokeLinejoin="round"
className={`relative ${additionalClasses}`} className={`relative ${additionalClasses}`}
> >
<path d="M14 19a6 6 0 0 0-12 0" /> <path d="M14 19a6 6 0 0 0-12 0" />

@ -14,9 +14,9 @@ export function AddedUserIcon(props: CheckIconProps) {
fill="none" fill="none"
className={`relative ${additionalClasses}`} className={`relative ${additionalClasses}`}
stroke="currentColor" stroke="currentColor"
stroke-width="2" strokeWidth="2"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" strokeLinejoin="round"
> >
<path d="M14 19a6 6 0 0 0-12 0" /> <path d="M14 19a6 6 0 0 0-12 0" />
<circle cx="8" cy="9" r="4" /> <circle cx="8" cy="9" r="4" />

@ -10,7 +10,7 @@ export function CheckIcon(props: CheckIconProps) {
className={`relative ${additionalClasses}`} className={`relative ${additionalClasses}`}
stroke="currentColor" stroke="currentColor"
fill="currentColor" fill="currentColor"
stroke-width="0" strokeWidth="0"
viewBox="0 0 16 16" viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >

@ -12,13 +12,13 @@ export function ChevronDownIcon(props: ChevronDownIconProps) {
width="24" width="24"
height="24" height="24"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" strokeWidth="1.5"
stroke="currentColor" stroke="currentColor"
className={className} className={className}
> >
<path <path
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" strokeLinejoin="round"
d="M19.5 8.25l-7.5 7.5-7.5-7.5" d="M19.5 8.25l-7.5 7.5-7.5-7.5"
/> />
</svg> </svg>

@ -12,7 +12,7 @@ export function CloseIcon(props: CloseIconProps) {
width="24" width="24"
height="24" height="24"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" strokeWidth="1.5"
stroke="currentColor" stroke="currentColor"
className={className} className={className}
> >

@ -14,9 +14,9 @@ export function DeleteUserIcon(props: CheckIconProps) {
fill="none" fill="none"
className={`relative ${additionalClasses}`} className={`relative ${additionalClasses}`}
stroke="currentColor" stroke="currentColor"
stroke-width="2" strokeWidth="2"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" strokeLinejoin="round"
> >
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /> <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" /> <circle cx="9" cy="7" r="4" />

@ -18,23 +18,23 @@ export function ErrorIcon(props: ErrorIconProps) {
d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"
fill="currentColor" fill="currentColor"
stroke="currentColor" stroke="currentColor"
stroke-width="2" strokeWidth="2"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" strokeLinejoin="round"
/> />
<path <path
d="M15 9L9 15" d="M15 9L9 15"
stroke="white" stroke="white"
stroke-width="2" strokeWidth="2"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" strokeLinejoin="round"
/> />
<path <path
d="M9 9L15 15" d="M9 9L15 15"
stroke="white" stroke="white"
stroke-width="2" strokeWidth="2"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" strokeLinejoin="round"
/> />
</svg> </svg>
); );

@ -18,23 +18,23 @@ export function InfoIcon(props: InfoIconProps) {
d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"
fill="currentColor" fill="currentColor"
stroke="currentColor" stroke="currentColor"
stroke-width="2" strokeWidth="2"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" strokeLinejoin="round"
/> />
<path <path
d="M12 16V12" d="M12 16V12"
stroke="white" stroke="white"
stroke-width="2" strokeWidth="2"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" strokeLinejoin="round"
/> />
<path <path
d="M12 8H12.01" d="M12 8H12.01"
stroke="white" stroke="white"
stroke-width="2" strokeWidth="2"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" strokeLinejoin="round"
/> />
</svg> </svg>
); );

@ -11,9 +11,9 @@ export function MailIcon(props: MailIconProps) {
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-width="2" strokeWidth="2"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" strokeLinejoin="round"
className={className} className={className}
> >
<rect width="20" height="16" x="2" y="4" rx="2" /> <rect width="20" height="16" x="2" y="4" rx="2" />

@ -11,9 +11,9 @@ export function ShareIcon(props: ShareIconProps) {
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-width="2" strokeWidth="2"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" strokeLinejoin="round"
{...props} {...props}
> >
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" /> <path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" />

@ -11,7 +11,6 @@ export function Spinner({
outerFill = '#404040', outerFill = '#404040',
innerFill = '#94a3b8', innerFill = '#94a3b8',
}: SpinnerProps) { }: SpinnerProps) {
className += className?.includes('w-') ? '' : ' w-3.5 h-3.5'; className += className?.includes('w-') ? '' : ' w-3.5 h-3.5';
return ( return (
@ -23,15 +22,15 @@ export function Spinner({
> >
{isDualRing && ( {isDualRing && (
<path <path
fill-rule="evenodd" fillRule="evenodd"
clip-rule="evenodd" clipRule="evenodd"
d="M46.5 93C72.1812 93 93 72.1812 93 46.5C93 20.8188 72.1812 0 46.5 0C20.8188 0 0 20.8188 0 46.5C0 72.1812 20.8188 93 46.5 93ZM46.5 77C63.3447 77 77 63.3447 77 46.5C77 29.6553 63.3447 16 46.5 16C29.6553 16 16 29.6553 16 46.5C16 63.3447 29.6553 77 46.5 77Z" d="M46.5 93C72.1812 93 93 72.1812 93 46.5C93 20.8188 72.1812 0 46.5 0C20.8188 0 0 20.8188 0 46.5C0 72.1812 20.8188 93 46.5 93ZM46.5 77C63.3447 77 77 63.3447 77 46.5C77 29.6553 63.3447 16 46.5 16C29.6553 16 16 29.6553 16 46.5C16 63.3447 29.6553 77 46.5 77Z"
style={`fill: ${outerFill};`} style={{ fill: outerFill }}
></path> ></path>
)} )}
<path <path
d="M84.9746 49.5667C89.3257 49.9135 93.2042 46.6479 92.81 42.3008C92.3588 37.3251 91.1071 32.437 89.0872 27.8298C86.0053 20.7998 81.2311 14.6422 75.1905 9.90623C69.15 5.17027 62.031 2.00329 54.4687 0.687889C49.5126 -0.174203 44.467 -0.223422 39.5274 0.525737C35.2118 1.18024 32.966 5.72596 34.3411 9.86865V9.86865C35.7161 14.0113 40.2118 16.1424 44.5681 15.8677C46.9635 15.7166 49.3773 15.8465 51.7599 16.2609C56.7515 17.1291 61.4505 19.2196 65.4377 22.3456C69.4249 25.4717 72.5762 29.5362 74.6105 34.1764C75.5815 36.3912 76.2835 38.7044 76.7084 41.0666C77.4811 45.3626 80.6234 49.2199 84.9746 49.5667V49.5667Z" d="M84.9746 49.5667C89.3257 49.9135 93.2042 46.6479 92.81 42.3008C92.3588 37.3251 91.1071 32.437 89.0872 27.8298C86.0053 20.7998 81.2311 14.6422 75.1905 9.90623C69.15 5.17027 62.031 2.00329 54.4687 0.687889C49.5126 -0.174203 44.467 -0.223422 39.5274 0.525737C35.2118 1.18024 32.966 5.72596 34.3411 9.86865V9.86865C35.7161 14.0113 40.2118 16.1424 44.5681 15.8677C46.9635 15.7166 49.3773 15.8465 51.7599 16.2609C56.7515 17.1291 61.4505 19.2196 65.4377 22.3456C69.4249 25.4717 72.5762 29.5362 74.6105 34.1764C75.5815 36.3912 76.2835 38.7044 76.7084 41.0666C77.4811 45.3626 80.6234 49.2199 84.9746 49.5667V49.5667Z"
style={`fill: ${innerFill};`} style={{ fill: innerFill }}
></path> ></path>
</svg> </svg>
); );

@ -13,9 +13,9 @@ export function StopIcon(props: CheckIconProps) {
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-width="2" strokeWidth="2"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" strokeLinejoin="round"
className={`relative ${additionalClasses}`} className={`relative ${additionalClasses}`}
> >
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="10" />

@ -12,9 +12,9 @@ export function TrashIcon(props: TrashIconProps) {
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-width="2" strokeWidth="2"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" strokeLinejoin="round"
className={className} className={className}
> >
<path d="M3 6h18" /> <path d="M3 6h18" />

@ -18,23 +18,23 @@ export function WarningIcon(props: WarningIconProps) {
d="M21.7304 18.0002L13.7304 4.00022C13.556 3.69243 13.303 3.43641 12.9973 3.25829C12.6917 3.08017 12.3442 2.98633 11.9904 2.98633C11.6366 2.98633 11.2892 3.08017 10.9835 3.25829C10.6778 3.43641 10.4249 3.69243 10.2504 4.00022L2.25042 18.0002C2.0741 18.3056 1.98165 18.6521 1.98243 19.0047C1.98321 19.3573 2.0772 19.7035 2.25486 20.008C2.43253 20.3126 2.68757 20.5648 2.99411 20.7391C3.30066 20.9133 3.64783 21.0034 4.00042 21.0002H20.0004C20.3513 20.9999 20.6959 20.9072 20.9997 20.7315C21.3035 20.5558 21.5556 20.3033 21.7309 19.9993C21.9062 19.6954 21.9985 19.3506 21.9984 18.9997C21.9983 18.6488 21.9059 18.3041 21.7304 18.0002Z" d="M21.7304 18.0002L13.7304 4.00022C13.556 3.69243 13.303 3.43641 12.9973 3.25829C12.6917 3.08017 12.3442 2.98633 11.9904 2.98633C11.6366 2.98633 11.2892 3.08017 10.9835 3.25829C10.6778 3.43641 10.4249 3.69243 10.2504 4.00022L2.25042 18.0002C2.0741 18.3056 1.98165 18.6521 1.98243 19.0047C1.98321 19.3573 2.0772 19.7035 2.25486 20.008C2.43253 20.3126 2.68757 20.5648 2.99411 20.7391C3.30066 20.9133 3.64783 21.0034 4.00042 21.0002H20.0004C20.3513 20.9999 20.6959 20.9072 20.9997 20.7315C21.3035 20.5558 21.5556 20.3033 21.7309 19.9993C21.9062 19.6954 21.9985 19.3506 21.9984 18.9997C21.9983 18.6488 21.9059 18.3041 21.7304 18.0002Z"
fill="currentColor" fill="currentColor"
stroke="currentColor" stroke="currentColor"
stroke-width="2" strokeWidth="2"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" strokeLinejoin="round"
/> />
<path <path
d="M12 9V13" d="M12 9V13"
stroke="white" stroke="white"
stroke-width="2" strokeWidth="2"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" strokeLinejoin="round"
/> />
<path <path
d="M12 17H12.01" d="M12 17H12.01"
stroke="white" stroke="white"
stroke-width="2" strokeWidth="2"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" strokeLinejoin="round"
/> />
</svg> </svg>
); );

@ -55,7 +55,7 @@ const { isSecondaryBanner = false, resourceId, resourceType } = Astro.props;
<ProgressShareButton <ProgressShareButton
resourceId={resourceId} resourceId={resourceId}
resourceType={resourceType} resourceType={resourceType}
client:load client:only="react"
/> />
<button <button
data-popup='progress-help' data-popup='progress-help'
@ -86,7 +86,7 @@ const { isSecondaryBanner = false, resourceId, resourceType } = Astro.props;
<ProgressShareButton <ProgressShareButton
resourceId={resourceId} resourceId={resourceId}
resourceType={resourceType} resourceType={resourceType}
client:load client:only="react"
/> />
</div> </div>
</div> </div>

@ -1,4 +1,4 @@
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
import { httpGet, httpPatch } from '../lib/http'; import { httpGet, httpPatch } from '../lib/http';
import BuildingIcon from '../icons/building.svg'; import BuildingIcon from '../icons/building.svg';
import ErrorIcon from '../icons/error.svg'; import ErrorIcon from '../icons/error.svg';
@ -92,7 +92,7 @@ export function RespondInviteForm() {
/> />
<h2 className={'mb-1 text-2xl font-bold'}>Error</h2> <h2 className={'mb-1 text-2xl font-bold'}>Error</h2>
<p class="mb-4 text-base leading-6 text-gray-600"> <p className="mb-4 text-base leading-6 text-gray-600">
{error || 'There was a problem, please try again.'} {error || 'There was a problem, please try again.'}
</p> </p>
@ -117,19 +117,19 @@ export function RespondInviteForm() {
/> />
<h2 className={'mb-1 text-2xl font-bold'}>Join Team</h2> <h2 className={'mb-1 text-2xl font-bold'}>Join Team</h2>
<p class="mb-3 text-base leading-6 text-gray-600"> <p className="mb-3 text-base leading-6 text-gray-600">
You have been invited to join the team{' '} You have been invited to join the team{' '}
<strong id="team-name">{invite?.team?.name}</strong>. <strong id="team-name">{invite?.team?.name}</strong>.
</p> </p>
{!isAuthenticated && ( {!isAuthenticated && (
<div class="mx-auto w-full duration-500 sm:max-w-md"> <div className="mx-auto w-full duration-500 sm:max-w-md">
<div class="flex w-full items-center gap-2"> <div className="flex w-full items-center gap-2">
<button <button
onClick={() => showLoginPopup()} onClick={() => showLoginPopup()}
data-popup="login-popup" data-popup="login-popup"
type="button" type="button"
class="flex-grow cursor-pointer rounded-lg bg-gray-200 px-3 py-2 text-center" className="flex-grow cursor-pointer rounded-lg bg-gray-200 px-3 py-2 text-center"
> >
Login to respond Login to respond
</button> </button>

@ -34,9 +34,8 @@ export function Editor(props: EditorProps) {
e.target.select(); e.target.select();
copyText(e.target.value); copyText(e.target.value);
}} }}
> value={text}
{text} />
</textarea>
</div> </div>
); );
} }

@ -1,4 +1,4 @@
import { useState } from 'preact/hooks'; import { useState } from 'react';
import { useCopyText } from '../../hooks/use-copy-text'; import { useCopyText } from '../../hooks/use-copy-text';
import { useAuth } from '../../hooks/use-auth'; import { useAuth } from '../../hooks/use-auth';
@ -113,7 +113,7 @@ export function RoadCardPage() {
<div className="flex items-start gap-4 mx-0 sm:-mx-10 px-0 sm:px-10 border-b py-4"> <div className="flex items-start gap-4 mx-0 sm:-mx-10 px-0 sm:px-10 border-b py-4">
<StepCounter step={4} /> <StepCounter step={4} />
<div class="flex-grow"> <div className="flex-grow">
<StepLabel label="Share your #RoadCard with others" /> <StepLabel label="Share your #RoadCard with others" />
<div className={'rounded-md border bg-gray-50 p-2 text-center'}> <div className={'rounded-md border bg-gray-50 p-2 text-center'}>
<a <a

@ -1,5 +1,5 @@
import { httpGet } from '../../lib/http'; import { httpGet } from '../../lib/http';
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
import { pageProgressMessage } from '../../stores/page'; import { pageProgressMessage } from '../../stores/page';
import type { UserProgressResponse } from '../HeroSection/FavoriteRoadmaps'; import type { UserProgressResponse } from '../HeroSection/FavoriteRoadmaps';
import { SelectionButton } from './SelectionButton'; import { SelectionButton } from './SelectionButton';
@ -33,19 +33,24 @@ export function RoadmapSelect(props: RoadmapSelectProps) {
}, []); }, []);
const canSelectMore = selectedRoadmaps.length < 4; const canSelectMore = selectedRoadmaps.length < 4;
const allProgress = progressList?.filter( const allProgress =
(progress) => progress.resourceType === 'roadmap' progressList?.filter((progress) => progress.resourceType === 'roadmap') ||
) || []; [];
return ( return (
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{allProgress?.length === 0 && <p className={'text-sm text-gray-400 italic'}>No progress tracked so far.</p>} {allProgress?.length === 0 && (
<p className={'text-sm italic text-gray-400'}>
No progress tracked so far.
</p>
)}
{allProgress?.map((progress) => { {allProgress?.map((progress) => {
const isSelected = selectedRoadmaps.includes(progress.resourceId); const isSelected = selectedRoadmaps.includes(progress.resourceId);
const canSelect = isSelected || canSelectMore; const canSelect = isSelected || canSelectMore;
return ( return (
<SelectionButton <SelectionButton
key={progress.resourceId}
text={progress.resourceTitle} text={progress.resourceTitle}
isDisabled={!canSelect} isDisabled={!canSelect}
isSelected={isSelected} isSelected={isSelected}

@ -46,7 +46,7 @@ const isRoadmapReady = !isUpcoming;
resourceId={roadmapId} resourceId={roadmapId}
resourceType='roadmap' resourceType='roadmap'
className='text-gray-500 !opacity-100 hover:text-gray-600 [&>svg]:stroke-[0.4] [&>svg]:stroke-gray-400 hover:[&>svg]:stroke-gray-600 [&>svg]:h-4 [&>svg]:w-4 sm:[&>svg]:h-5 sm:[&>svg]:w-5 ml-1.5 relative focus:outline-0' className='text-gray-500 !opacity-100 hover:text-gray-600 [&>svg]:stroke-[0.4] [&>svg]:stroke-gray-400 hover:[&>svg]:stroke-gray-600 [&>svg]:h-4 [&>svg]:w-4 sm:[&>svg]:h-5 sm:[&>svg]:w-5 ml-1.5 relative focus:outline-0'
client:load client:only="react"
/> />
</h1> </h1>
<p class='text-sm text-gray-500 sm:text-lg'>{description}</p> <p class='text-sm text-gray-500 sm:text-lg'>{description}</p>

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'react';
export type OptionType = { export type OptionType = {
value: string; value: string;
@ -118,12 +118,12 @@ export function SearchSelector({
{isActive && ( {isActive && (
<div <div
class="absolute top-full z-50 mt-2 w-full rounded-md bg-gray-100 px-2 py-2" className="absolute top-full z-50 mt-2 w-full rounded-md bg-gray-100 px-2 py-2"
ref={dropdownRef} ref={dropdownRef}
> >
<div className="flex flex-col"> <div className="flex flex-col">
{searchResults.length === 0 && ( {searchResults.length === 0 && (
<div class="p-5 text-center text-sm text-gray-400"> <div className="p-5 text-center text-sm text-gray-400">
No results found No results found
</div> </div>
)} )}
@ -133,7 +133,7 @@ export function SearchSelector({
<> <>
<button <button
type="button" type="button"
class={`flex w-full items-center rounded p-2 text-sm ${ className={`flex w-full items-center rounded p-2 text-sm ${
counter === activeCounter ? 'bg-gray-200' : '' counter === activeCounter ? 'bg-gray-200' : ''
}`} }`}
onMouseOver={() => setActiveCounter(counter)} onMouseOver={() => setActiveCounter(counter)}

@ -34,7 +34,7 @@ export function Stepper(props: StepperProps) {
/> />
)} )}
{!isComplete && ( {!isComplete && (
<span class="mr-2 font-semibold">{stepCounter + 1}</span> <span className="mr-2 font-semibold">{stepCounter + 1}</span>
)} )}
<span className="flex flex-grow">{step.label}</span> <span className="flex flex-grow">{step.label}</span>
</li> </li>

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'react';
import ChevronDown from '../../icons/dropdown.svg'; import ChevronDown from '../../icons/dropdown.svg';
import { httpGet } from '../../lib/http'; import { httpGet } from '../../lib/http';
import { useAuth } from '../../hooks/use-auth'; import { useAuth } from '../../hooks/use-auth';
@ -6,7 +6,7 @@ import { useOutsideClick } from '../../hooks/use-outside-click';
import { Spinner } from '../ReactIcons/Spinner'; import { Spinner } from '../ReactIcons/Spinner';
import type { AllowedRoles } from '../CreateTeam/RoleDropdown'; import type { AllowedRoles } from '../CreateTeam/RoleDropdown';
import { $currentTeam, $teamList } from '../../stores/team'; import { $currentTeam, $teamList } from '../../stores/team';
import { useStore } from '@nanostores/preact'; import { useStore } from '@nanostores/react';
import { useTeamId } from '../../hooks/use-team-id'; import { useTeamId } from '../../hooks/use-team-id';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import type { ValidTeamType } from '../CreateTeam/Step0'; import type { ValidTeamType } from '../CreateTeam/Step0';
@ -103,10 +103,10 @@ export function TeamDropdown() {
<span>Choose Team</span> <span>Choose Team</span>
{shouldShowTeamsIndicator && ( {shouldShowTeamsIndicator && (
<span class="mr-1 inline-flex h-1 w-1 items-center justify-center font-medium text-blue-300"> <span className="mr-1 inline-flex h-1 w-1 items-center justify-center font-medium text-blue-300">
<span class="relative flex items-center"> <span className="relative flex items-center">
<span class="relative rounded-full bg-gray-200 p-1 text-xs" /> <span className="relative rounded-full bg-gray-200 p-1 text-xs" />
<span class="absolute bottom-0 left-0 right-0 top-0 animate-ping rounded-full bg-gray-400 p-1 text-xs" /> <span className="absolute bottom-0 left-0 right-0 top-0 animate-ping rounded-full bg-gray-400 p-1 text-xs" />
</span> </span>
</span> </span>
)} )}
@ -166,7 +166,7 @@ export function TeamDropdown() {
} }
return ( return (
<li> <li key={team?._id}>
<a <a
className="flex w-full cursor-pointer items-center gap-2 rounded p-2 text-sm font-medium text-slate-100 hover:bg-slate-700" className="flex w-full cursor-pointer items-center gap-2 rounded p-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
href={`${pageLink}`} href={`${pageLink}`}
@ -194,7 +194,7 @@ export function TeamDropdown() {
</div> </div>
)} )}
</div> </div>
<hr class="my-4" /> <hr className="my-4" />
</> </>
); );
} }

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'preact/hooks'; import { FormEvent, useEffect, useRef, useState } from 'react';
import { httpPost } from '../../lib/http'; import { httpPost } from '../../lib/http';
import { useTeamId } from '../../hooks/use-team-id'; import { useTeamId } from '../../hooks/use-team-id';
import { useOutsideClick } from '../../hooks/use-outside-click'; import { useOutsideClick } from '../../hooks/use-outside-click';
@ -24,7 +24,7 @@ export function InviteMemberPopup(props: InviteMemberPopupProps) {
emailRef?.current?.focus(); emailRef?.current?.focus();
}, []); }, []);
const handleSubmit = async (e: Event) => { const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
setIsLoading(true); setIsLoading(true);
setError(''); setError('');
@ -55,19 +55,21 @@ export function InviteMemberPopup(props: InviteMemberPopupProps) {
useOutsideClick(popupBodyRef, handleClosePopup); useOutsideClick(popupBodyRef, handleClosePopup);
return ( return (
<div class="popup fixed left-0 right-0 top-0 z-50 flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50"> <div className="popup fixed left-0 right-0 top-0 z-50 flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50">
<div class="relative h-full w-full max-w-md p-4 md:h-auto"> <div className="relative h-full w-full max-w-md p-4 md:h-auto">
<div <div
ref={popupBodyRef} ref={popupBodyRef}
class="popup-body relative rounded-lg bg-white p-4 shadow" className="popup-body relative rounded-lg bg-white p-4 shadow"
> >
<h3 class="mb-1.5 text-xl sm:text-2xl font-medium">Invite Member</h3> <h3 className="mb-1.5 text-xl font-medium sm:text-2xl">
<p className="mb-3 text-sm leading-none text-gray-400 hidden sm:block"> Invite Member
</h3>
<p className="mb-3 hidden text-sm leading-none text-gray-400 sm:block">
Enter the email and role below to invite a member. Enter the email and role below to invite a member.
</p> </p>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="mt-0 sm:mt-4 my-4 flex flex-col gap-2"> <div className="my-4 mt-0 flex flex-col gap-2 sm:mt-4">
<input <input
ref={emailRef} ref={emailRef}
type="email" type="email"
@ -108,7 +110,7 @@ export function InviteMemberPopup(props: InviteMemberPopupProps) {
<button <button
type="submit" type="submit"
disabled={isLoading || !email} disabled={isLoading || !email}
class="flex-grow cursor-pointer rounded-lg bg-black py-2 text-center text-white disabled:opacity-40" className="flex-grow cursor-pointer rounded-lg bg-black py-2 text-center text-white disabled:opacity-40"
> >
{isLoading ? 'Please wait ..' : 'Invite'} {isLoading ? 'Please wait ..' : 'Invite'}
</button> </button>

@ -1,4 +1,4 @@
import { useState } from 'preact/hooks'; import { useState } from 'react';
import { LeaveTeamPopup } from './LeaveTeamPopup'; import { LeaveTeamPopup } from './LeaveTeamPopup';
type LeaveTeamButtonProps = { type LeaveTeamButtonProps = {

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'react';
import { httpDelete } from '../../lib/http'; import { httpDelete } from '../../lib/http';
import { useTeamId } from '../../hooks/use-team-id'; import { useTeamId } from '../../hooks/use-team-id';
import { useOutsideClick } from '../../hooks/use-outside-click'; import { useOutsideClick } from '../../hooks/use-outside-click';
@ -59,13 +59,13 @@ export function LeaveTeamPopup(props: LeaveTeamPopupProps) {
useOutsideClick(popupBodyRef, handleClosePopup); useOutsideClick(popupBodyRef, handleClosePopup);
return ( return (
<div class="popup fixed left-0 right-0 top-0 z-50 flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50"> <div className="popup fixed left-0 right-0 top-0 z-50 flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50">
<div class="relative h-full w-full max-w-md p-4 md:h-auto"> <div className="relative h-full w-full max-w-md p-4 md:h-auto">
<div <div
ref={popupBodyRef} ref={popupBodyRef}
class="popup-body relative rounded-lg bg-white p-4 shadow" className="popup-body relative rounded-lg bg-white p-4 shadow"
> >
<h2 class="text-2xl font-semibold text-black"> <h2 className="text-2xl font-semibold text-black">
Leave Team Leave Team
</h2> </h2>
<p className="text-gray-500"> <p className="text-gray-500">

@ -1,4 +1,4 @@
import { useRef, useState } from 'preact/hooks'; import { useRef, useState } from 'react';
import type { TeamMemberDocument } from './TeamMembersPage'; import type { TeamMemberDocument } from './TeamMembersPage';
import MoreIcon from '../../icons/more-vertical.svg'; import MoreIcon from '../../icons/more-vertical.svg';
import { useOutsideClick } from '../../hooks/use-outside-click'; import { useOutsideClick } from '../../hooks/use-outside-click';

@ -3,7 +3,7 @@ import { MemberActionDropdown } from './MemberActionDropdown';
import { MemberRoleBadge } from './RoleBadge'; import { MemberRoleBadge } from './RoleBadge';
import type { TeamMemberItem } from './TeamMembersPage'; import type { TeamMemberItem } from './TeamMembersPage';
import { $canManageCurrentTeam } from '../../stores/team'; import { $canManageCurrentTeam } from '../../stores/team';
import { useStore } from '@nanostores/preact'; import { useStore } from '@nanostores/react';
type TeamMemberProps = { type TeamMemberProps = {
member: TeamMemberItem; member: TeamMemberItem;
@ -91,7 +91,7 @@ export function TeamMemberItem(props: TeamMemberProps) {
</div> </div>
<div className="flex shrink-0 items-center text-sm"> <div className="flex shrink-0 items-center text-sm">
<span class={'hidden sm:block'}> <span className={'hidden sm:block'}>
<MemberRoleBadge role={member.role} /> <MemberRoleBadge role={member.role} />
</span> </span>
{canManageCurrentTeam && ( {canManageCurrentTeam && (

@ -1,4 +1,4 @@
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
import { httpDelete, httpGet, httpPatch } from '../../lib/http'; import { httpDelete, httpGet, httpPatch } from '../../lib/http';
import { useAuth } from '../../hooks/use-auth'; import { useAuth } from '../../hooks/use-auth';
import { pageProgressMessage } from '../../stores/page'; import { pageProgressMessage } from '../../stores/page';
@ -9,7 +9,7 @@ import type { AllowedMemberStatus } from '../TeamDropdown/TeamDropdown';
import { InviteMemberPopup } from './InviteMemberPopup'; import { InviteMemberPopup } from './InviteMemberPopup';
import { getUrlParams } from '../../lib/browser'; import { getUrlParams } from '../../lib/browser';
import { UpdateMemberPopup } from './UpdateMemberPopup'; import { UpdateMemberPopup } from './UpdateMemberPopup';
import { useStore } from '@nanostores/preact'; import { useStore } from '@nanostores/react';
import { $canManageCurrentTeam } from '../../stores/team'; import { $canManageCurrentTeam } from '../../stores/team';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import { TeamMemberItem } from './TeamMemberItem'; import { TeamMemberItem } from './TeamMemberItem';

@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'preact/hooks'; import { FormEvent, useRef, useState } from 'react';
import { httpPost, httpPut } from '../../lib/http'; import { httpPut } from '../../lib/http';
import { useTeamId } from '../../hooks/use-team-id'; import { useTeamId } from '../../hooks/use-team-id';
import { useOutsideClick } from '../../hooks/use-outside-click'; import { useOutsideClick } from '../../hooks/use-outside-click';
import { AllowedRoles, RoleDropdown } from '../CreateTeam/RoleDropdown'; import { AllowedRoles, RoleDropdown } from '../CreateTeam/RoleDropdown';
@ -20,7 +20,7 @@ export function UpdateMemberPopup(props: InviteMemberPopupProps) {
const [error, setError] = useState(''); const [error, setError] = useState('');
const { teamId } = useTeamId(); const { teamId } = useTeamId();
const handleSubmit = async (e: Event) => { const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
setIsLoading(true); setIsLoading(true);
setError(''); setError('');
@ -52,19 +52,21 @@ export function UpdateMemberPopup(props: InviteMemberPopupProps) {
useOutsideClick(popupBodyRef, handleClosePopup); useOutsideClick(popupBodyRef, handleClosePopup);
return ( return (
<div class="popup fixed left-0 right-0 top-0 z-50 flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50"> <div className="popup fixed left-0 right-0 top-0 z-50 flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50">
<div class="relative h-full w-full max-w-md p-4 md:h-auto"> <div className="relative h-full w-full max-w-md p-4 md:h-auto">
<div <div
ref={popupBodyRef} ref={popupBodyRef}
class="popup-body relative rounded-lg bg-white p-4 shadow" className="popup-body relative rounded-lg bg-white p-4 shadow"
> >
<h3 class="mb-1.5 text-xl sm:text-2xl font-medium">Update Role</h3> <h3 className="mb-1.5 text-xl font-medium sm:text-2xl">
<p className="mb-3 text-sm leading-none text-gray-400 hidden sm:block"> Update Role
</h3>
<p className="mb-3 hidden text-sm leading-none text-gray-400 sm:block">
Select the role to update for this member Select the role to update for this member
</p> </p>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="mt-0 sm:mt-4 my-4 flex flex-col gap-2"> <div className="my-4 mt-0 flex flex-col gap-2 sm:mt-4">
<span className="mt-2 block w-full rounded-md bg-gray-100 p-2"> <span className="mt-2 block w-full rounded-md bg-gray-100 p-2">
{member.invitedEmail} {member.invitedEmail}
</span> </span>
@ -96,7 +98,7 @@ export function UpdateMemberPopup(props: InviteMemberPopupProps) {
<button <button
type="submit" type="submit"
disabled={isLoading || !selectedRole} disabled={isLoading || !selectedRole}
class="flex-grow cursor-pointer rounded-lg bg-black py-2 text-center text-white disabled:opacity-40" className="flex-grow cursor-pointer rounded-lg bg-black py-2 text-center text-white disabled:opacity-40"
> >
{isLoading ? 'Please wait ..' : 'Update Role'} {isLoading ? 'Please wait ..' : 'Update Role'}
</button> </button>

@ -1,4 +1,4 @@
import { useState } from 'preact/hooks'; import { useState } from 'react';
import type { GroupByRoadmap, TeamMember } from './TeamProgressPage'; import type { GroupByRoadmap, TeamMember } from './TeamProgressPage';
import { getUrlParams } from '../../lib/browser'; import { getUrlParams } from '../../lib/browser';
import ExternalLinkIcon from '../../icons/external-link.svg'; import ExternalLinkIcon from '../../icons/external-link.svg';

@ -1,5 +1,5 @@
import type { TeamMember } from './TeamProgressPage'; import type { TeamMember } from './TeamProgressPage';
import { useState } from 'preact/hooks'; import { useState } from 'react';
type MemberProgressItemProps = { type MemberProgressItemProps = {
member: TeamMember; member: TeamMember;
@ -29,7 +29,7 @@ export function MemberProgressItem(props: MemberProgressItemProps) {
: '/images/default-avatar.png' : '/images/default-avatar.png'
} }
alt={member.name || ''} alt={member.name || ''}
className="h-8 w-8 rounded-full" className="min-w-[32px] min-h-[32px] h-8 w-8 rounded-full"
/> />
<div className="inline-grid w-full"> <div className="inline-grid w-full">
{!isMyProgress && ( {!isMyProgress && (

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'react';
import { wireframeJSONToSVG } from 'roadmap-renderer'; import { wireframeJSONToSVG } from 'roadmap-renderer';
import { Spinner } from '../ReactIcons/Spinner'; import { Spinner } from '../ReactIcons/Spinner';
import '../FrameRenderer/FrameRenderer.css'; import '../FrameRenderer/FrameRenderer.css';
@ -16,7 +16,7 @@ import CloseIcon from '../../icons/close.svg';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import { useAuth } from '../../hooks/use-auth'; import { useAuth } from '../../hooks/use-auth';
import { pageProgressMessage } from '../../stores/page'; import { pageProgressMessage } from '../../stores/page';
import { useStore } from '@nanostores/preact'; import { useStore } from '@nanostores/react';
import { $currentTeam } from '../../stores/team'; import { $currentTeam } from '../../stores/team';
export type ProgressMapProps = { export type ProgressMapProps = {
@ -277,14 +277,14 @@ export function MemberProgressModal(props: ProgressMapProps) {
const progressPercentage = Math.round((memberDone / memberTotal) * 100); const progressPercentage = Math.round((memberDone / memberTotal) * 100);
return ( return (
<div class="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50"> <div className="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50">
<div <div
id={currentTeam?.type === 'company' ? 'customized-roadmap' : 'original-roadmap'} id={currentTeam?.type === 'company' ? 'customized-roadmap' : 'original-roadmap'}
class="relative mx-auto h-full w-full max-w-4xl p-4 md:h-auto" className="relative mx-auto h-full w-full max-w-4xl p-4 md:h-auto"
> >
<div <div
ref={popupBodyEl} ref={popupBodyEl}
class="popup-body relative rounded-lg bg-white pt-[1px] shadow" className="popup-body relative rounded-lg bg-white pt-[1px] shadow"
> >
{isCurrentUser && ( {isCurrentUser && (
<div className="sticky top-1 mx-1 mb-0 mt-1 rounded-xl bg-gray-900 p-4 text-gray-300"> <div className="sticky top-1 mx-1 mb-0 mt-1 rounded-xl bg-gray-900 p-4 text-gray-300">
@ -344,11 +344,11 @@ export function MemberProgressModal(props: ProgressMapProps) {
</div> </div>
)} )}
<p <p
class={`-mx-4 mb-3 flex items-center justify-start border-b border-t px-4 py-2 text-sm sm:hidden ${ className={`-mx-4 mb-3 flex items-center justify-start border-b border-t px-4 py-2 text-sm sm:hidden ${
isLoading ? 'striped-loader' : '' isLoading ? 'striped-loader' : ''
}`} }`}
> >
<span class="mr-2.5 block rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900"> <span className="mr-2.5 block rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900">
<span>{progressPercentage}</span>% Done <span>{progressPercentage}</span>% Done
</span> </span>
@ -357,18 +357,18 @@ export function MemberProgressModal(props: ProgressMapProps) {
</span> </span>
</p> </p>
<p <p
class={`-mx-4 mb-3 hidden items-center justify-center border-b border-t py-2 text-sm sm:flex ${ className={`-mx-4 mb-3 hidden items-center justify-center border-b border-t py-2 text-sm sm:flex ${
isLoading ? 'striped-loader' : '' isLoading ? 'striped-loader' : ''
}`} }`}
> >
<span class="mr-2.5 block rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900"> <span className="mr-2.5 block rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900">
<span>{progressPercentage}</span>% Done <span>{progressPercentage}</span>% Done
</span> </span>
<span> <span>
<span>{memberDone}</span> completed <span>{memberDone}</span> completed
</span> </span>
<span class="mx-1.5 text-gray-400">·</span> <span className="mx-1.5 text-gray-400">·</span>
<span> <span>
<span data-progress-learning="">{memberLearning}</span> in <span data-progress-learning="">{memberLearning}</span> in
progress progress
@ -376,7 +376,7 @@ export function MemberProgressModal(props: ProgressMapProps) {
{memberSkipped > 0 && ( {memberSkipped > 0 && (
<> <>
<span class="mx-1.5 text-gray-400">·</span> <span className="mx-1.5 text-gray-400">·</span>
<span> <span>
<span data-progress-skipped="">{memberSkipped}</span>{' '} <span data-progress-skipped="">{memberSkipped}</span>{' '}
skipped skipped
@ -384,7 +384,7 @@ export function MemberProgressModal(props: ProgressMapProps) {
</> </>
)} )}
<span class="mx-1.5 text-gray-400">·</span> <span className="mx-1.5 text-gray-400">·</span>
<span> <span>
<span data-progress-total="">{memberTotal}</span> Total <span data-progress-total="">{memberTotal}</span> Total
</span> </span>
@ -398,7 +398,7 @@ export function MemberProgressModal(props: ProgressMapProps) {
></div> ></div>
{isLoading && ( {isLoading && (
<div class="flex w-full justify-center"> <div className="flex w-full justify-center">
<Spinner <Spinner
isDualRing={false} isDualRing={false}
className="mb-4 mt-2 h-4 w-4 animate-spin fill-blue-600 text-gray-200 sm:h-8 sm:w-8" className="mb-4 mt-2 h-4 w-4 animate-spin fill-blue-600 text-gray-200 sm:h-8 sm:w-8"
@ -414,7 +414,7 @@ export function MemberProgressModal(props: ProgressMapProps) {
onClick={onClose} onClick={onClose}
> >
<img alt={'close'} src={CloseIcon} className="h-4 w-4" /> <img alt={'close'} src={CloseIcon} className="h-4 w-4" />
<span class="sr-only">Close modal</span> <span className="sr-only">Close modal</span>
</button> </button>
</div> </div>
</div> </div>

@ -1,10 +1,9 @@
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
import { useTeamId } from '../../hooks/use-team-id';
import { httpGet } from '../../lib/http'; import { httpGet } from '../../lib/http';
import { pageProgressMessage } from '../../stores/page'; import { pageProgressMessage } from '../../stores/page';
import { MemberProgressItem } from './MemberProgressItem'; import { MemberProgressItem } from './MemberProgressItem';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import { useStore } from '@nanostores/preact'; import { useStore } from '@nanostores/react';
import { $currentTeam } from '../../stores/team'; import { $currentTeam } from '../../stores/team';
import { GroupRoadmapItem } from './GroupRoadmapItem'; import { GroupRoadmapItem } from './GroupRoadmapItem';
import { getUrlParams, setUrlParams } from '../../lib/browser'; import { getUrlParams, setUrlParams } from '../../lib/browser';
@ -49,8 +48,7 @@ const groupingTypes = [
] as const; ] as const;
export function TeamProgressPage() { export function TeamProgressPage() {
const { teamId } = useTeamId(); const { t: teamId, gb: groupBy } = getUrlParams();
const { gb: groupBy } = getUrlParams();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const toast = useToast(); const toast = useToast();
@ -100,15 +98,6 @@ export function TeamProgressPage() {
}); });
}, [teamId]); }, [teamId]);
if (isLoading) {
return null;
}
if (!teamId) {
window.location.href = '/';
return;
}
useEffect(() => { useEffect(() => {
if (!selectedGrouping) { if (!selectedGrouping) {
return; return;
@ -145,6 +134,15 @@ export function TeamProgressPage() {
}); });
} }
if (!teamId) {
window.location.href = '/';
return;
}
if (isLoading) {
return null;
}
return ( return (
<div> <div>
{showMemberProgress && ( {showMemberProgress && (
@ -170,6 +168,7 @@ export function TeamProgressPage() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{groupingTypes.map((grouping) => ( {groupingTypes.map((grouping) => (
<button <button
key={grouping.value}
className={`rounded-md border p-1 px-2 text-sm ${ className={`rounded-md border p-1 px-2 text-sm ${
selectedGrouping === grouping.value selectedGrouping === grouping.value
? ' border-gray-400 bg-gray-200 ' ? ' border-gray-400 bg-gray-200 '
@ -205,6 +204,7 @@ export function TeamProgressPage() {
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4 sm:grid-cols-2">
{teamMembers.map((member) => ( {teamMembers.map((member) => (
<MemberProgressItem <MemberProgressItem
key={member._id}
member={member} member={member}
isMyProgress={member?.email === user?.email} isMyProgress={member?.email === user?.email}
onShowResourceProgress={(resourceId) => { onShowResourceProgress={(resourceId) => {

@ -1,5 +1,5 @@
import { getUrlParams } from '../lib/browser'; import { getUrlParams } from '../lib/browser';
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
import type { TeamDocument } from './CreateTeam/CreateTeamForm'; import type { TeamDocument } from './CreateTeam/CreateTeamForm';
import type { TeamResourceConfig } from './CreateTeam/RoadmapSelector'; import type { TeamResourceConfig } from './CreateTeam/RoadmapSelector';
import { httpGet, httpPut } from '../lib/http'; import { httpGet, httpPut } from '../lib/http';
@ -9,7 +9,7 @@ import RoadmapIcon from '../icons/roadmap.svg';
import PlusIcon from '../icons/plus.svg'; import PlusIcon from '../icons/plus.svg';
import type { PageType } from './CommandMenu/CommandMenu'; import type { PageType } from './CommandMenu/CommandMenu';
import { UpdateTeamResourceModal } from './CreateTeam/UpdateTeamResourceModal'; import { UpdateTeamResourceModal } from './CreateTeam/UpdateTeamResourceModal';
import { useStore } from '@nanostores/preact'; import { useStore } from '@nanostores/react';
import { $canManageCurrentTeam } from '../stores/team'; import { $canManageCurrentTeam } from '../stores/team';
import { useToast } from '../hooks/use-toast'; import { useToast } from '../hooks/use-toast';
import { SelectRoadmapModal } from './CreateTeam/SelectRoadmapModal'; import { SelectRoadmapModal } from './CreateTeam/SelectRoadmapModal';
@ -248,7 +248,7 @@ export function TeamRoadmaps() {
'...'; '...';
return ( return (
<div className="flex flex-col items-start rounded-md border border-gray-300"> <div key={resourceId} className="flex flex-col items-start rounded-md border border-gray-300">
<div className={'w-full px-3 py-4'}> <div className={'w-full px-3 py-4'}>
<a <a
href={`/${resourceId}?t=${teamId}`} href={`/${resourceId}?t=${teamId}`}

@ -1,14 +1,13 @@
import { useEffect, useState } from 'preact/hooks'; import { FormEvent, useEffect, useState } from 'react';
import { httpGet, httpPut } from '../../lib/http'; import { httpGet, httpPut } from '../../lib/http';
import { Spinner } from '../ReactIcons/Spinner'; import { Spinner } from '../ReactIcons/Spinner';
import { useAuth } from '../../hooks/use-auth';
import UploadProfilePicture from '../UpdateProfile/UploadProfilePicture'; import UploadProfilePicture from '../UpdateProfile/UploadProfilePicture';
import type { TeamDocument } from '../CreateTeam/CreateTeamForm'; import type { TeamDocument } from '../CreateTeam/CreateTeamForm';
import { pageProgressMessage } from '../../stores/page'; import { pageProgressMessage } from '../../stores/page';
import { useTeamId } from '../../hooks/use-team-id'; import { useTeamId } from '../../hooks/use-team-id';
import { DeleteTeamPopup } from '../DeleteTeamPopup'; import { DeleteTeamPopup } from '../DeleteTeamPopup';
import { $currentTeam, $isCurrentTeamAdmin } from '../../stores/team'; import { $isCurrentTeamAdmin } from '../../stores/team';
import { useStore } from '@nanostores/preact'; import { useStore } from '@nanostores/react';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
export function UpdateTeamForm() { export function UpdateTeamForm() {
@ -44,7 +43,7 @@ export function UpdateTeamForm() {
setIsDisabled(!isCurrentTeamAdmin); setIsDisabled(!isCurrentTeamAdmin);
}, [isCurrentTeamAdmin]); }, [isCurrentTeamAdmin]);
const handleSubmit = async (e: Event) => { const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
setIsLoading(true); setIsLoading(true);
if (!name || !teamType) { if (!name || !teamType) {
@ -123,7 +122,7 @@ export function UpdateTeamForm() {
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="mt-4 flex w-full flex-col"> <div className="mt-4 flex w-full flex-col">
<label <label
for="name" htmlFor="name"
className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]' className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]'
> >
Name Name
@ -142,7 +141,7 @@ export function UpdateTeamForm() {
</div> </div>
<div className="mt-4 flex w-full flex-col"> <div className="mt-4 flex w-full flex-col">
<label <label
for="website" htmlFor="website"
className={`text-sm leading-none text-slate-500 ${ className={`text-sm leading-none text-slate-500 ${
teamType === 'company' ? 'after:content-["*"]' : '' teamType === 'company' ? 'after:content-["*"]' : ''
}`} }`}
@ -164,7 +163,7 @@ export function UpdateTeamForm() {
{teamType === 'company' && ( {teamType === 'company' && (
<div className="mt-4 flex w-full flex-col"> <div className="mt-4 flex w-full flex-col">
<label <label
for="linkedIn" htmlFor="linkedIn"
className="text-sm leading-none text-slate-500" className="text-sm leading-none text-slate-500"
> >
LinkedIn URL LinkedIn URL
@ -182,7 +181,10 @@ export function UpdateTeamForm() {
</div> </div>
)} )}
<div className="mt-4 flex w-full flex-col"> <div className="mt-4 flex w-full flex-col">
<label for="gitHub" className="text-sm leading-none text-slate-500"> <label
htmlFor="gitHub"
className="text-sm leading-none text-slate-500"
>
GitHub URL GitHub URL
</label> </label>
<input <input
@ -198,7 +200,7 @@ export function UpdateTeamForm() {
</div> </div>
<div className="mt-4 flex w-full flex-col"> <div className="mt-4 flex w-full flex-col">
<label <label
for="type" htmlFor="type"
className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]' className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]'
> >
Type Type
@ -208,12 +210,12 @@ export function UpdateTeamForm() {
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 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="" selected> <option value="">
Select type Select type
</option> </option>
<option value="company">Company</option> <option value="company">Company</option>
@ -224,7 +226,7 @@ export function UpdateTeamForm() {
{teamType === 'company' && ( {teamType === 'company' && (
<div className="mt-4 flex w-full flex-col"> <div className="mt-4 flex w-full flex-col">
<label <label
for="team-size" htmlFor="team-size"
className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]' className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]'
> >
Team size Team size
@ -269,7 +271,7 @@ export function UpdateTeamForm() {
{isCurrentTeamAdmin && ( {isCurrentTeamAdmin && (
<> <>
<hr class="my-8" /> <hr className="my-8" />
{isDeleting && ( {isDeleting && (
<DeleteTeamPopup <DeleteTeamPopup
onClose={() => { onClose={() => {
@ -277,15 +279,15 @@ export function UpdateTeamForm() {
}} }}
/> />
)} )}
<h2 class="text-xl font-bold sm:text-2xl">Delete Team</h2> <h2 className="text-xl font-bold sm:text-2xl">Delete Team</h2>
<p class="mt-2 text-gray-400"> <p className="mt-2 text-gray-400">
Permanently delete this team and all of its resources. Permanently delete this team and all of its resources.
</p> </p>
<button <button
onClick={() => setIsDeleting(true)} onClick={() => setIsDeleting(true)}
data-popup="delete-team-popup" data-popup="delete-team-popup"
class="font-regular mt-4 w-full rounded-lg bg-red-600 py-2 text-base text-white outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-1" className="font-regular mt-4 w-full rounded-lg bg-red-600 py-2 text-base text-white outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-1"
> >
Delete Team Delete Team
</button> </button>

@ -1,4 +1,3 @@
import type { FunctionalComponent } from 'preact';
import { TeamDropdown } from './TeamDropdown/TeamDropdown'; import { TeamDropdown } from './TeamDropdown/TeamDropdown';
import ChevronDown from '../icons/dropdown.svg'; import ChevronDown from '../icons/dropdown.svg';
import { useTeamId } from '../hooks/use-team-id'; import { useTeamId } from '../hooks/use-team-id';
@ -7,14 +6,18 @@ import SettingsIcon from '../icons/cog.svg';
import ChatIcon from '../icons/chat.svg'; import ChatIcon from '../icons/chat.svg';
import MapIcon from '../icons/map.svg'; import MapIcon from '../icons/map.svg';
import GroupIcon from '../icons/group.svg'; import GroupIcon from '../icons/group.svg';
import { useState } from 'preact/hooks'; import { useState } from 'react';
import { useStore } from '@nanostores/preact'; import type { ReactNode } from 'react';
import { useStore } from '@nanostores/react';
import { $currentTeam } from '../stores/team'; import { $currentTeam } from '../stores/team';
import { SubmitFeedbackPopup } from './Feedback/SubmitFeedbackPopup'; import { SubmitFeedbackPopup } from './Feedback/SubmitFeedbackPopup';
export const TeamSidebar: FunctionalComponent<{ type TeamSidebarProps = {
activePageId: string; activePageId: string;
}> = ({ activePageId, children }) => { children: ReactNode;
};
export function TeamSidebar({ activePageId, children }: TeamSidebarProps) {
const [menuShown, setMenuShown] = useState(false); const [menuShown, setMenuShown] = useState(false);
const currentTeam = useStore($currentTeam); const currentTeam = useStore($currentTeam);
const [showFeedbackPopup, setShowFeedbackPopup] = useState(false); const [showFeedbackPopup, setShowFeedbackPopup] = useState(false);
@ -51,9 +54,9 @@ export const TeamSidebar: FunctionalComponent<{
return ( return (
<> <>
<div class="relative mb-5 block border-b p-4 shadow-inner md:hidden"> <div className="relative mb-5 block border-b p-4 shadow-inner md:hidden">
<button <button
class="flex h-10 w-full items-center justify-between rounded-md border bg-white px-2 text-center text-sm font-medium text-gray-900" className="flex h-10 w-full items-center justify-between rounded-md border bg-white px-2 text-center text-sm font-medium text-gray-900"
id="settings-menu" id="settings-menu"
aria-haspopup="true" aria-haspopup="true"
aria-expanded="true" aria-expanded="true"
@ -63,21 +66,21 @@ export const TeamSidebar: FunctionalComponent<{
sidebarLinks.find((sidebarLink) => sidebarLink.id === activePageId) sidebarLinks.find((sidebarLink) => sidebarLink.id === activePageId)
?.title ?.title
} }
<img alt="menu" src={ChevronDown} class="h-4 w-4" /> <img alt="menu" src={ChevronDown} className="h-4 w-4" />
</button> </button>
{menuShown && ( {menuShown && (
<ul <ul
id="settings-menu-dropdown" id="settings-menu-dropdown"
class="absolute left-0 right-0 z-50 mt-1 space-y-1.5 bg-white p-2 shadow-lg" className="absolute left-0 right-0 z-50 mt-1 space-y-1.5 bg-white p-2 shadow-lg"
> >
<li> <li>
<a <a
href="/team" href="/team"
class={`flex w-full items-center rounded px-3 py-1.5 text-sm text-slate-900 hover:bg-slate-200 ${ className={`flex w-full items-center rounded px-3 py-1.5 text-sm text-slate-900 hover:bg-slate-200 ${
activePageId === 'team' ? 'bg-slate-100' : '' activePageId === 'team' ? 'bg-slate-100' : ''
}`} }`}
> >
<img alt={'teams'} src={GroupIcon} class={`mr-2 h-4 w-4`} /> <img alt={'teams'} src={GroupIcon} className={`mr-2 h-4 w-4`} />
Personal Account / Teams Personal Account / Teams
</a> </a>
</li> </li>
@ -85,10 +88,10 @@ export const TeamSidebar: FunctionalComponent<{
const isActive = activePageId === sidebarLink.id; const isActive = activePageId === sidebarLink.id;
return ( return (
<li> <li key={sidebarLink.id}>
<a <a
href={sidebarLink.href} href={sidebarLink.href}
class={`flex w-full items-center rounded px-3 py-1.5 text-sm text-slate-900 hover:bg-slate-200 ${ className={`flex w-full items-center rounded px-3 py-1.5 text-sm text-slate-900 hover:bg-slate-200 ${
isActive ? 'bg-slate-100' : '' isActive ? 'bg-slate-100' : ''
}`} }`}
> >
@ -127,25 +130,25 @@ export const TeamSidebar: FunctionalComponent<{
/> />
)} )}
<div class="container flex min-h-screen items-stretch"> <div className="container flex min-h-screen items-stretch">
<aside class="hidden w-44 shrink-0 border-r border-slate-200 py-10 md:block"> <aside className="hidden w-44 shrink-0 border-r border-slate-200 py-10 md:block">
<TeamDropdown /> <TeamDropdown />
<nav> <nav>
<ul class="space-y-1"> <ul className="space-y-1">
{sidebarLinks.map((sidebarLink) => { {sidebarLinks.map((sidebarLink) => {
const isActive = activePageId === sidebarLink.id; const isActive = activePageId === sidebarLink.id;
return ( return (
<li> <li key={sidebarLink.id}>
<a <a
href={sidebarLink.href} href={sidebarLink.href}
class={`font-regular flex w-full items-center border-r-2 px-2 py-1.5 text-sm ${ className={`font-regular flex w-full items-center border-r-2 px-2 py-1.5 text-sm ${
isActive isActive
? 'border-r-black bg-gray-100 text-black' ? 'border-r-black bg-gray-100 text-black'
: 'border-r-transparent text-gray-500 hover:border-r-gray-300' : 'border-r-transparent text-gray-500 hover:border-r-gray-300'
}`} }`}
> >
<span class="flex flex-grow items-center justify-between"> <span className="flex flex-grow items-center justify-between">
<span className="flex"> <span className="flex">
<img <img
alt="menu icon" alt="menu icon"
@ -155,9 +158,9 @@ export const TeamSidebar: FunctionalComponent<{
{sidebarLink.title} {sidebarLink.title}
</span> </span>
{sidebarLink.hasWarning && ( {sidebarLink.hasWarning && (
<span class="relative mr-1 flex items-center"> <span className="relative mr-1 flex items-center">
<span class="relative rounded-full bg-red-200 p-1 text-xs" /> <span className="relative rounded-full bg-red-200 p-1 text-xs" />
<span class="absolute bottom-0 left-0 right-0 top-0 animate-ping rounded-full bg-red-400 p-1 text-xs" /> <span className="absolute bottom-0 left-0 right-0 top-0 animate-ping rounded-full bg-red-400 p-1 text-xs" />
</span> </span>
)} )}
</span> </span>
@ -171,7 +174,7 @@ export const TeamSidebar: FunctionalComponent<{
className="mr-3 mt-4 flex items-center justify-center rounded-md border p-2 text-sm text-gray-500 transition-colors hover:border-gray-300 hover:bg-gray-50 hover:text-black" className="mr-3 mt-4 flex items-center justify-center rounded-md border p-2 text-sm text-gray-500 transition-colors hover:border-gray-300 hover:bg-gray-50 hover:text-black"
onClick={() => setShowFeedbackPopup(true)} onClick={() => setShowFeedbackPopup(true)}
> >
<img src={ChatIcon} className="mr-2 h-4 w-4" /> <img alt={'feedback'} src={ChatIcon} className="mr-2 h-4 w-4" />
Send Feedback Send Feedback
</button> </button>
</nav> </nav>
@ -180,4 +183,4 @@ export const TeamSidebar: FunctionalComponent<{
</div> </div>
</> </>
); );
}; }

@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from 'preact/hooks'; import { useState, useEffect, useRef } from 'react';
import type { TeamDocument } from '../CreateTeam/CreateTeamForm'; import type { TeamDocument } from '../CreateTeam/CreateTeamForm';
import type { TeamResourceConfig } from '../CreateTeam/RoadmapSelector'; import type { TeamResourceConfig } from '../CreateTeam/RoadmapSelector';
import { httpGet } from '../../lib/http'; import { httpGet } from '../../lib/http';
@ -44,7 +44,8 @@ export function TeamVersions(props: TeamVersionsProps) {
const [selectedTeamVersion, setSelectedTeamVersion] = useState< const [selectedTeamVersion, setSelectedTeamVersion] = useState<
TeamVersionsResponse[0] | null TeamVersionsResponse[0] | null
>(null); >(null);
let shouldShowAvatar = true;
let shouldShowAvatar: boolean;
const selectedAvatar = selectedTeamVersion const selectedAvatar = selectedTeamVersion
? selectedTeamVersion.team.avatar ? selectedTeamVersion.team.avatar
: user?.avatar; : user?.avatar;
@ -55,10 +56,8 @@ export function TeamVersions(props: TeamVersionsProps) {
// Show avatar if team has one, or if user has one otherwise use first letter of name // Show avatar if team has one, or if user has one otherwise use first letter of name
if (selectedTeamVersion?.team.avatar) { if (selectedTeamVersion?.team.avatar) {
shouldShowAvatar = true; shouldShowAvatar = true;
} else if (!selectedTeamVersion && user?.avatar) {
shouldShowAvatar = true;
} else { } else {
shouldShowAvatar = false; shouldShowAvatar = !!(!selectedTeamVersion && user?.avatar);
} }
useOutsideClick(teamDropdownRef, () => { useOutsideClick(teamDropdownRef, () => {
@ -105,10 +104,6 @@ export function TeamVersions(props: TeamVersionsProps) {
}); });
}, []); }, []);
if (isPreparing) {
return null;
}
useEffect(() => { useEffect(() => {
clearResourceProgress(); clearResourceProgress();
if (!selectedTeamVersion) { if (!selectedTeamVersion) {
@ -127,6 +122,10 @@ export function TeamVersions(props: TeamVersionsProps) {
}); });
}, [selectedTeamVersion]); }, [selectedTeamVersion]);
if (isPreparing) {
return null;
}
if (!teamVersions.length) { if (!teamVersions.length) {
return null; return null;
} }
@ -145,8 +144,8 @@ export function TeamVersions(props: TeamVersionsProps) {
</span> </span>
<img <img
alt="Dropdown" alt="Dropdown"
src={DropdownIcon} src={DropdownIcon as any}
class="h-3 w-3 sm:h-4 sm:w-4" className="h-3 w-3 sm:h-4 sm:w-4"
/> />
</div> </div>
<div className="sm:hidden"> <div className="sm:hidden">
@ -192,12 +191,13 @@ export function TeamVersions(props: TeamVersionsProps) {
<span className="truncate">Personal</span> <span className="truncate">Personal</span>
</div> </div>
</button> </button>
{teamVersions.map((team) => { {teamVersions.map((team: TeamVersionsResponse[0]) => {
const isSelectedTeam = const isSelectedTeam =
selectedTeamVersion?.team._id === team.team._id; selectedTeamVersion?.team._id === team.team._id;
return ( return (
<button <button
key={team?.team?._id}
className={`flex h-8 w-full items-center justify-between px-3 py-1.5 text-xs font-medium hover:bg-gray-100 sm:text-sm ${ className={`flex h-8 w-full items-center justify-between px-3 py-1.5 text-xs font-medium hover:bg-gray-100 sm:text-sm ${
isSelectedTeam ? 'bg-gray-100' : 'bg-white' isSelectedTeam ? 'bg-gray-100' : 'bg-white'
}`} }`}

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'react';
import ChevronDown from '../icons/dropdown.svg'; import ChevronDown from '../icons/dropdown.svg';
import { httpGet } from '../lib/http'; import { httpGet } from '../lib/http';
import { useTeamId } from '../hooks/use-team-id'; import { useTeamId } from '../hooks/use-team-id';
@ -33,7 +33,7 @@ export function TeamsList() {
}, []); }, []);
return ( return (
<div className="relative max-w-[500px] mx-auto"> <div className="relative mx-auto max-w-[500px]">
<div className="w-full px-2 py-2"> <div className="w-full px-2 py-2">
<div className={'mb-8 hidden md:block'}> <div className={'mb-8 hidden md:block'}>
<h2 className={'text-3xl font-bold sm:text-4xl'}>Teams</h2> <h2 className={'text-3xl font-bold sm:text-4xl'}>Teams</h2>
@ -41,7 +41,7 @@ export function TeamsList() {
Here are the teams you are part of Here are the teams you are part of
</p> </p>
</div> </div>
<ul class="mb-3 flex flex-col gap-1"> <ul className="mb-3 flex flex-col gap-1">
<li> <li>
<a <a
className="flex w-full cursor-pointer items-center justify-between gap-2 truncate rounded border p-2 text-sm font-medium hover:border-gray-300 hover:bg-gray-50" className="flex w-full cursor-pointer items-center justify-between gap-2 truncate rounded border p-2 text-sm font-medium hover:border-gray-300 hover:bg-gray-50"
@ -65,7 +65,7 @@ export function TeamsList() {
</a> </a>
</li> </li>
{teamList.map((team) => ( {teamList.map((team) => (
<li> <li key={team._id}>
<a <a
className="flex w-full cursor-pointer items-center justify-between gap-2 rounded border p-2 text-sm font-medium hover:border-gray-300 hover:bg-gray-50" className="flex w-full cursor-pointer items-center justify-between gap-2 rounded border p-2 text-sm font-medium hover:border-gray-300 hover:bg-gray-50"
href={`/team/progress?t=${team._id}`} href={`/team/progress?t=${team._id}`}
@ -93,7 +93,7 @@ export function TeamsList() {
className="inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-none focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400" className="inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-none focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400"
href="/team/new" href="/team/new"
> >
<span class='mr-2'>+</span> <span className="mr-2">+</span>
<span>New Team</span> <span>New Team</span>
</a> </a>
</div> </div>

@ -1,6 +1,6 @@
import { useStore } from '@nanostores/preact'; import { useStore } from '@nanostores/react';
import { $toastMessage } from '../stores/toast'; import { $toastMessage } from '../stores/toast';
import { useEffect } from 'preact/hooks'; import { useEffect } from 'react';
import { CheckIcon } from './ReactIcons/CheckIcon'; import { CheckIcon } from './ReactIcons/CheckIcon';
import { ErrorIcon } from './ReactIcons/ErrorIcon'; import { ErrorIcon } from './ReactIcons/ErrorIcon';
import { WarningIcon } from './ReactIcons/WarningIcon'; import { WarningIcon } from './ReactIcons/WarningIcon';

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'react';
import { httpPost } from '../../lib/http'; import { httpPost } from '../../lib/http';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
@ -157,7 +157,7 @@ export function ContributionForm(props: ContributionFormProps) {
<div> <div>
<div className="mb-2 mt-2 rounded-md border bg-gray-100 p-3"> <div className="mb-2 mt-2 rounded-md border bg-gray-100 p-3">
<h1 className="mb-2 text-2xl font-bold">Guidelines</h1> <h1 className="mb-2 text-2xl font-bold">Guidelines</h1>
<ul class="flex flex-col gap-1 text-sm text-gray-700"> <ul className="flex flex-col gap-1 text-sm text-gray-700">
<li>Content should only be in English.</li> <li>Content should only be in English.</li>
<li>Do not add things you have not evaluated personally.</li> <li>Do not add things you have not evaluated personally.</li>
<li>It should strictly be relevant to the topic.</li> <li>It should strictly be relevant to the topic.</li>

@ -1,4 +1,4 @@
import { useMemo, useRef, useState } from 'preact/hooks'; import { useMemo, useRef, useState } from 'react';
import CloseIcon from '../../icons/close.svg'; import CloseIcon from '../../icons/close.svg';
import SpinnerIcon from '../../icons/spinner.svg'; import SpinnerIcon from '../../icons/spinner.svg';
@ -147,7 +147,7 @@ export function TopicDetail() {
{isLoading && ( {isLoading && (
<div className="flex w-full justify-center"> <div className="flex w-full justify-center">
<img <img
src={SpinnerIcon} src={SpinnerIcon as any}
alt="Loading" alt="Loading"
className="h-6 w-6 animate-spin fill-blue-600 text-gray-200 sm:h-12 sm:w-12" className="h-6 w-6 animate-spin fill-blue-600 text-gray-200 sm:h-12 sm:w-12"
/> />
@ -192,7 +192,7 @@ export function TopicDetail() {
setIsContributing(false); setIsContributing(false);
}} }}
> >
<img alt="Close" class="h-5 w-5" src={CloseIcon} /> <img alt="Close" className="h-5 w-5" src={CloseIcon as any} />
</button> </button>
</div> </div>
@ -205,7 +205,7 @@ export function TopicDetail() {
{/* Contribution */} {/* Contribution */}
<div className="mt-8 flex-1 border-t"> <div className="mt-8 flex-1 border-t">
<p class="mb-2 mt-2 text-sm leading-relaxed text-gray-400"> <p className="mb-2 mt-2 text-sm leading-relaxed text-gray-400">
Help others learn by submitting links to learn more about this topic{' '} Help others learn by submitting links to learn more about this topic{' '}
</p> </p>
<button <button
@ -229,7 +229,7 @@ export function TopicDetail() {
</> </>
)} )}
</div> </div>
<div class="fixed inset-0 z-30 bg-gray-900 bg-opacity-50 dark:bg-opacity-80"></div> <div className="fixed inset-0 z-30 bg-gray-900 bg-opacity-50 dark:bg-opacity-80"></div>
</div> </div>
); );
} }

@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; import { useEffect, useMemo, useRef, useState } from 'react';
import { useKeydown } from '../../hooks/use-keydown'; import { useKeydown } from '../../hooks/use-keydown';
import { useOutsideClick } from '../../hooks/use-outside-click'; import { useOutsideClick } from '../../hooks/use-outside-click';
import DownIcon from '../../icons/down.svg'; import DownIcon from '../../icons/down.svg';
@ -165,7 +165,7 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
if (isUpdatingProgress) { if (isUpdatingProgress) {
return ( return (
<button className="inline-flex cursor-default items-center rounded-md border border-gray-300 bg-white p-1 px-2 text-sm text-black"> <button className="inline-flex cursor-default items-center rounded-md border border-gray-300 bg-white p-1 px-2 text-sm text-black">
<img alt="Check" class="h-4 w-4 animate-spin" src={SpinnerIcon} /> <img alt="Check" className="h-4 w-4 animate-spin" src={SpinnerIcon} />
<span className="ml-2">Updating Status..</span> <span className="ml-2">Updating Status..</span>
</button> </button>
); );
@ -174,9 +174,9 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
return ( return (
<div className="relative inline-flex rounded-md border border-gray-300"> <div className="relative inline-flex rounded-md border border-gray-300">
<span className="inline-flex cursor-default items-center p-1 px-2 text-sm text-black"> <span className="inline-flex cursor-default items-center p-1 px-2 text-sm text-black">
<span class="flex h-2 w-2"> <span className="flex h-2 w-2">
<span <span
class={`relative inline-flex h-2 w-2 rounded-full ${statusColors[progress]}`} className={`relative inline-flex h-2 w-2 rounded-full ${statusColors[progress]}`}
></span> ></span>
</span> </span>
<span className="ml-2 capitalize"> <span className="ml-2 capitalize">
@ -189,7 +189,7 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
onClick={() => setShowChangeStatus(true)} onClick={() => setShowChangeStatus(true)}
> >
<span className="mr-0.5">Update Status</span> <span className="mr-0.5">Update Status</span>
<img alt="Check" class="h-4 w-4" src={DownIcon} /> <img alt="Check" className="h-4 w-4" src={DownIcon} />
</button> </button>
{showChangeStatus && ( {showChangeStatus && (
@ -199,59 +199,59 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
> >
{allowMarkingDone && ( {allowMarkingDone && (
<button <button
class="inline-flex justify-between px-3 py-1.5 text-left text-sm text-gray-800 hover:bg-gray-100" className="inline-flex justify-between px-3 py-1.5 text-left text-sm text-gray-800 hover:bg-gray-100"
onClick={() => handleUpdateResourceProgress('done')} onClick={() => handleUpdateResourceProgress('done')}
> >
<span> <span>
<span <span
class={`mr-2 inline-block h-2 w-2 rounded-full ${statusColors['done']}`} className={`mr-2 inline-block h-2 w-2 rounded-full ${statusColors['done']}`}
></span> ></span>
Done Done
</span> </span>
<span class="text-xs text-gray-500">D</span> <span className="text-xs text-gray-500">D</span>
</button> </button>
)} )}
{allowMarkingLearning && ( {allowMarkingLearning && (
<button <button
class="inline-flex justify-between px-3 py-1.5 text-left text-sm text-gray-800 hover:bg-gray-100" className="inline-flex justify-between px-3 py-1.5 text-left text-sm text-gray-800 hover:bg-gray-100"
onClick={() => handleUpdateResourceProgress('learning')} onClick={() => handleUpdateResourceProgress('learning')}
> >
<span> <span>
<span <span
class={`mr-2 inline-block h-2 w-2 rounded-full ${statusColors['learning']}`} className={`mr-2 inline-block h-2 w-2 rounded-full ${statusColors['learning']}`}
></span> ></span>
In Progress In Progress
</span> </span>
<span class="text-xs text-gray-500">L</span> <span className="text-xs text-gray-500">L</span>
</button> </button>
)} )}
{allowMarkingPending && ( {allowMarkingPending && (
<button <button
class="inline-flex justify-between px-3 py-1.5 text-left text-sm text-gray-800 hover:bg-gray-100" className="inline-flex justify-between px-3 py-1.5 text-left text-sm text-gray-800 hover:bg-gray-100"
onClick={() => handleUpdateResourceProgress('pending')} onClick={() => handleUpdateResourceProgress('pending')}
> >
<span> <span>
<span <span
class={`mr-2 inline-block h-2 w-2 rounded-full ${statusColors['pending']}`} className={`mr-2 inline-block h-2 w-2 rounded-full ${statusColors['pending']}`}
></span> ></span>
Reset Reset
</span> </span>
<span class="text-xs text-gray-500">R</span> <span className="text-xs text-gray-500">R</span>
</button> </button>
)} )}
{allowMarkingSkipped && ( {allowMarkingSkipped && (
<button <button
class="inline-flex justify-between px-3 py-1.5 text-left text-sm text-gray-800 hover:bg-gray-100" className="inline-flex justify-between px-3 py-1.5 text-left text-sm text-gray-800 hover:bg-gray-100"
onClick={() => handleUpdateResourceProgress('skipped')} onClick={() => handleUpdateResourceProgress('skipped')}
> >
<span> <span>
<span <span
class={`mr-2 inline-block h-2 w-2 rounded-full ${statusColors['skipped']}`} className={`mr-2 inline-block h-2 w-2 rounded-full ${statusColors['skipped']}`}
></span> ></span>
Skip Skip
</span> </span>
<span class="text-xs text-gray-500">S</span> <span className="text-xs text-gray-500">S</span>
</button> </button>
)} )}
</div> </div>

@ -1,4 +1,4 @@
import { useEffect, useState } from 'preact/hooks'; import { FormEvent, useEffect, useState } from 'react';
import { httpGet, httpPost } from '../../lib/http'; import { httpGet, httpPost } from '../../lib/http';
import { pageProgressMessage } from '../../stores/page'; import { pageProgressMessage } from '../../stores/page';
@ -13,7 +13,7 @@ export default function UpdatePasswordForm() {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const handleSubmit = async (e: Event) => { const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
setIsLoading(true); setIsLoading(true);
setError(''); setError('');
@ -78,15 +78,17 @@ export default function UpdatePasswordForm() {
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div class="hidden md:block mb-8"> <div className="mb-8 hidden md:block">
<h2 className="text-3xl font-bold sm:text-4xl">Password</h2> <h2 className="text-3xl font-bold sm:text-4xl">Password</h2>
<p className="mt-2 text-gray-400">Use the form below to update your password.</p> <p className="mt-2 text-gray-400">
Use the form below to update your password.
</p>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
{authProvider === 'email' && ( {authProvider === 'email' && (
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<label <label
for="current-password" htmlFor="current-password"
className="text-sm leading-none text-slate-500" className="text-sm leading-none text-slate-500"
> >
Current Password Current Password
@ -96,6 +98,7 @@ export default function UpdatePasswordForm() {
type="password" type="password"
name="current-password" name="current-password"
id="current-password" id="current-password"
autoComplete={"current-password"}
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 disabled:bg-gray-100" 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 disabled:bg-gray-100"
required required
minLength={6} minLength={6}
@ -110,7 +113,7 @@ export default function UpdatePasswordForm() {
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<label <label
for="new-password" htmlFor="new-password"
className="text-sm leading-none text-slate-500" className="text-sm leading-none text-slate-500"
> >
New Password New Password
@ -119,6 +122,7 @@ export default function UpdatePasswordForm() {
type="password" type="password"
name="new-password" name="new-password"
id="new-password" id="new-password"
autoComplete={"new-password"}
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 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 required
minLength={6} minLength={6}
@ -131,7 +135,7 @@ export default function UpdatePasswordForm() {
</div> </div>
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<label <label
for="new-password-confirmation" htmlFor="new-password-confirmation"
className="text-sm leading-none text-slate-500" className="text-sm leading-none text-slate-500"
> >
Confirm New Password Confirm New Password
@ -141,6 +145,7 @@ export default function UpdatePasswordForm() {
name="new-password-confirmation" name="new-password-confirmation"
id="new-password-confirmation" id="new-password-confirmation"
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 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"
autoComplete={"new-password"}
required required
minLength={6} minLength={6}
placeholder="Confirm New Password" placeholder="Confirm New Password"
@ -152,11 +157,11 @@ export default function UpdatePasswordForm() {
</div> </div>
{error && ( {error && (
<p class="mt-2 rounded-lg bg-red-100 p-2 text-red-700">{error}</p> <p className="mt-2 rounded-lg bg-red-100 p-2 text-red-700">{error}</p>
)} )}
{success && ( {success && (
<p class="mt-2 rounded-lg bg-green-100 p-2 text-green-700"> <p className="mt-2 rounded-lg bg-green-100 p-2 text-green-700">
{success} {success}
</p> </p>
)} )}

@ -1,4 +1,4 @@
import { useEffect, useState } from 'preact/hooks'; import { FormEvent, useEffect, useState } from 'react';
import { httpGet, httpPost } from '../../lib/http'; import { httpGet, httpPost } from '../../lib/http';
import { pageProgressMessage } from '../../stores/page'; import { pageProgressMessage } from '../../stores/page';
import UploadProfilePicture from './UploadProfilePicture'; import UploadProfilePicture from './UploadProfilePicture';
@ -16,7 +16,7 @@ export function UpdateProfileForm() {
const [error, setError] = useState(''); const [error, setError] = useState('');
const [success, setSuccess] = useState(''); const [success, setSuccess] = useState('');
const handleSubmit = async (e: Event) => { const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
setIsLoading(true); setIsLoading(true);
setError(''); setError('');
@ -97,7 +97,7 @@ export function UpdateProfileForm() {
<form className="mt-4 space-y-4" onSubmit={handleSubmit}> <form className="mt-4 space-y-4" onSubmit={handleSubmit}>
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<label <label
for="name" htmlFor="name"
className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]' className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]'
> >
Name Name
@ -115,7 +115,7 @@ export function UpdateProfileForm() {
</div> </div>
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<label <label
for="email" htmlFor="email"
className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]' className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]'
> >
Email Email
@ -133,7 +133,10 @@ export function UpdateProfileForm() {
</div> </div>
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<label for="github" className="text-sm leading-none text-slate-500"> <label
htmlFor="github"
className="text-sm leading-none text-slate-500"
>
Github Github
</label> </label>
<input <input
@ -147,7 +150,10 @@ export function UpdateProfileForm() {
/> />
</div> </div>
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<label for="twitter" className="text-sm leading-none text-slate-500"> <label
htmlFor="twitter"
className="text-sm leading-none text-slate-500"
>
Twitter Twitter
</label> </label>
<input <input
@ -162,7 +168,10 @@ export function UpdateProfileForm() {
</div> </div>
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<label for="linkedin" className="text-sm leading-none text-slate-500"> <label
htmlFor="linkedin"
className="text-sm leading-none text-slate-500"
>
LinkedIn LinkedIn
</label> </label>
<input <input
@ -177,7 +186,10 @@ export function UpdateProfileForm() {
</div> </div>
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<label for="website" className="text-sm leading-none text-slate-500"> <label
htmlFor="website"
className="text-sm leading-none text-slate-500"
>
Website Website
</label> </label>
<input <input

@ -1,5 +1,5 @@
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { useEffect, useRef, useState } from 'preact/hooks'; import { ChangeEvent, FormEvent, useEffect, useRef, useState } from 'react';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt'; import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
interface PreviewFile extends File { interface PreviewFile extends File {
@ -60,7 +60,7 @@ export default function UploadProfilePicture(props: UploadProfilePictureProps) {
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const onImageChange = async (e: Event) => { const onImageChange = async (e: ChangeEvent<HTMLInputElement>) => {
setError(''); setError('');
const file = (e.target as HTMLInputElement).files?.[0]; const file = (e.target as HTMLInputElement).files?.[0];
@ -81,7 +81,7 @@ export default function UploadProfilePicture(props: UploadProfilePictureProps) {
); );
}; };
const handleSubmit = async (e: Event) => { const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
setError(''); setError('');
setIsLoading(true); setIsLoading(true);

@ -3,6 +3,7 @@ import { useCopyText } from '../../hooks/use-copy-text';
import type { ResourceType } from '../../lib/resource-progress'; import type { ResourceType } from '../../lib/resource-progress';
import { CheckIcon } from '../ReactIcons/CheckIcon'; import { CheckIcon } from '../ReactIcons/CheckIcon';
import { ShareIcon } from '../ReactIcons/ShareIcon'; import { ShareIcon } from '../ReactIcons/ShareIcon';
import { isLoggedIn } from '../../lib/jwt';
type ProgressShareButtonProps = { type ProgressShareButtonProps = {
resourceId: string; resourceId: string;

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'react';
import { wireframeJSONToSVG } from 'roadmap-renderer'; import { wireframeJSONToSVG } from 'roadmap-renderer';
import '../FrameRenderer/FrameRenderer.css'; import '../FrameRenderer/FrameRenderer.css';
import { useOutsideClick } from '../../hooks/use-outside-click'; import { useOutsideClick } from '../../hooks/use-outside-click';
@ -43,12 +43,7 @@ export function UserProgressModal(props: ProgressMapProps) {
const resourceSvgEl = useRef<HTMLDivElement>(null); const resourceSvgEl = useRef<HTMLDivElement>(null);
const popupBodyEl = useRef<HTMLDivElement>(null); const popupBodyEl = useRef<HTMLDivElement>(null);
const currentUser = useAuth(); const currentUser = useAuth();
if (!userId || currentUser?.id === userId) {
deleteUrlParam('s');
return null;
}
const [showModal, setShowModal] = useState(!!userId); const [showModal, setShowModal] = useState(!!userId);
const [resourceSvg, setResourceSvg] = useState<SVGElement | null>(null); const [resourceSvg, setResourceSvg] = useState<SVGElement | null>(null);
@ -114,7 +109,7 @@ export function UserProgressModal(props: ProgressMapProps) {
}); });
useEffect(() => { useEffect(() => {
if (!resourceJsonUrl || !resourceId || !resourceType) { if (!resourceJsonUrl || !resourceId || !resourceType || !userId) {
return; return;
} }
@ -182,14 +177,19 @@ export function UserProgressModal(props: ProgressMapProps) {
const userLearning = progress?.learning?.length || 0; const userLearning = progress?.learning?.length || 0;
const userSkipped = progress?.skipped?.length || 0; const userSkipped = progress?.skipped?.length || 0;
if (!userId || currentUser?.id === userId) {
deleteUrlParam('s');
return null;
}
if (!showModal) { if (!showModal) {
return null; return null;
} }
if (isLoading || error) { if (isLoading || error) {
return ( return (
<div class="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50"> <div className="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50">
<div class="relative mx-auto flex h-full w-full items-center justify-center"> <div className="relative mx-auto flex h-full w-full items-center justify-center">
<div className="popup-body relative rounded-lg bg-white p-5 shadow"> <div className="popup-body relative rounded-lg bg-white p-5 shadow">
<div className="flex items-center"> <div className="flex items-center">
{isLoading && ( {isLoading && (
@ -217,12 +217,12 @@ export function UserProgressModal(props: ProgressMapProps) {
return ( return (
<div <div
id={'user-progress-modal'} id={'user-progress-modal'}
class="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50" className="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50"
> >
<div class="relative mx-auto h-full w-full max-w-4xl p-4 md:h-auto"> <div className="relative mx-auto h-full w-full max-w-4xl p-4 md:h-auto">
<div <div
ref={popupBodyEl} ref={popupBodyEl}
class={`popup-body relative rounded-lg bg-white pt-[1px] shadow`} className={`popup-body relative rounded-lg bg-white pt-[1px] shadow`}
> >
<div className="p-4"> <div className="p-4">
<div className="mb-5 mt-0 min-h-[28px] text-left sm:text-center md:mt-4 md:h-[60px]"> <div className="mb-5 mt-0 min-h-[28px] text-left sm:text-center md:mt-4 md:h-[60px]">
@ -238,9 +238,9 @@ export function UserProgressModal(props: ProgressMapProps) {
</p> </p>
</div> </div>
<p <p
class={`-mx-4 mb-3 flex items-center justify-start border-b border-t px-4 py-2 text-sm sm:hidden`} className={`-mx-4 mb-3 flex items-center justify-start border-b border-t px-4 py-2 text-sm sm:hidden`}
> >
<span class="mr-2.5 block rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900"> <span className="mr-2.5 block rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900">
<span>{progressPercentage}</span>% Done <span>{progressPercentage}</span>% Done
</span> </span>
@ -249,32 +249,32 @@ export function UserProgressModal(props: ProgressMapProps) {
</span> </span>
</p> </p>
<p <p
class={`-mx-4 mb-3 hidden items-center justify-center border-b border-t py-2 text-sm sm:flex ${ className={`-mx-4 mb-3 hidden items-center justify-center border-b border-t py-2 text-sm sm:flex ${
isLoading ? 'striped-loader' : '' isLoading ? 'striped-loader' : ''
}`} }`}
> >
<span class="mr-2.5 block rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900"> <span className="mr-2.5 block rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900">
<span>{progressPercentage}</span>% Done <span>{progressPercentage}</span>% Done
</span> </span>
<span> <span>
<span>{userDone}</span> completed <span>{userDone}</span> completed
</span> </span>
<span class="mx-1.5 text-gray-400">·</span> <span className="mx-1.5 text-gray-400">·</span>
<span> <span>
<span>{userLearning}</span> in progress <span>{userLearning}</span> in progress
</span> </span>
{userSkipped > 0 && ( {userSkipped > 0 && (
<> <>
<span class="mx-1.5 text-gray-400">·</span> <span className="mx-1.5 text-gray-400">·</span>
<span> <span>
<span>{userSkipped}</span> skipped <span>{userSkipped}</span> skipped
</span> </span>
</> </>
)} )}
<span class="mx-1.5 text-gray-400">·</span> <span className="mx-1.5 text-gray-400">·</span>
<span> <span>
<span>{userProgressTotal}</span> Total <span>{userProgressTotal}</span> Total
</span> </span>
@ -292,8 +292,8 @@ export function UserProgressModal(props: ProgressMapProps) {
className={`absolute right-2.5 top-3 ml-auto inline-flex items-center rounded-lg bg-gray-100 bg-transparent p-1.5 text-sm text-gray-400 hover:text-gray-900 lg:hidden`} className={`absolute right-2.5 top-3 ml-auto inline-flex items-center rounded-lg bg-gray-100 bg-transparent p-1.5 text-sm text-gray-400 hover:text-gray-900 lg:hidden`}
onClick={onClose} onClick={onClose}
> >
<img alt={'close'} src={CloseIcon} className="h-4 w-4" /> <img alt={'close'} src={CloseIcon as any} className="h-4 w-4" />
<span class="sr-only">Close modal</span> <span className="sr-only">Close modal</span>
</button> </button>
</div> </div>
</div> </div>

@ -1,4 +1,4 @@
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
export function useCopyText() { export function useCopyText() {
const [isCopied, setIsCopied] = useState(false); const [isCopied, setIsCopied] = useState(false);

@ -1,4 +1,4 @@
import { useEffect } from 'preact/hooks'; import { useEffect } from 'react';
export function useKeydown(keyName: string, callback: any, deps: any[] = []) { export function useKeydown(keyName: string, callback: any, deps: any[] = []) {
useEffect(() => { useEffect(() => {

@ -1,4 +1,4 @@
import { useEffect } from 'preact/hooks'; import { useEffect } from 'react';
import type { ResourceType } from '../lib/resource-progress'; import type { ResourceType } from '../lib/resource-progress';
type CallbackType = (data: { type CallbackType = (data: {

@ -1,4 +1,4 @@
import { useEffect } from 'preact/hooks'; import { useEffect } from 'react';
export function useOutsideClick(ref: any, callback: any) { export function useOutsideClick(ref: any, callback: any) {
useEffect(() => { useEffect(() => {

@ -1,4 +1,4 @@
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "react";
export function useParams<T = Record<string, any>>(): T { export function useParams<T = Record<string, any>>(): T {
const [params, setParams] = useState<T>({} as T); const [params, setParams] = useState<T>({} as T);

@ -1,4 +1,4 @@
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'react';
export function useTeamId() { export function useTeamId() {
const [teamId, setTeamId] = useState<string | null>(null); const [teamId, setTeamId] = useState<string | null>(null);

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save