mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-30 20:49:49 +02:00
Refactor and add upgrade button on dashboard
This commit is contained in:
@@ -4,28 +4,53 @@ import { showLoginPopup } from '../../../lib/popup';
|
|||||||
import { cn } from '../../../lib/classname';
|
import { cn } from '../../../lib/classname';
|
||||||
import { CreateRoadmapModal } from './CreateRoadmapModal';
|
import { CreateRoadmapModal } from './CreateRoadmapModal';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useIsPaidUser } from '../../../queries/billing';
|
||||||
|
import { UpgradeAccountModal } from '../../Billing/UpgradeAccountModal';
|
||||||
|
import { MAX_ROADMAP_LIMIT } from '../RoadmapListPage';
|
||||||
|
|
||||||
type CreateRoadmapButtonProps = {
|
type CreateRoadmapButtonProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
existingRoadmapCount?: number;
|
||||||
text?: string;
|
text?: string;
|
||||||
teamId?: string;
|
teamId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CreateRoadmapButton(props: CreateRoadmapButtonProps) {
|
export function CreateRoadmapButton(props: CreateRoadmapButtonProps) {
|
||||||
const { teamId, className, text = 'Create your own Roadmap' } = props;
|
const {
|
||||||
|
teamId,
|
||||||
|
className,
|
||||||
|
text = 'Create your own Roadmap',
|
||||||
|
existingRoadmapCount = 0,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false);
|
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false);
|
||||||
|
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
|
||||||
|
const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
|
||||||
|
|
||||||
function toggleCreateRoadmapHandler() {
|
function toggleCreateRoadmapHandler() {
|
||||||
if (!isLoggedIn()) {
|
if (!isLoggedIn()) {
|
||||||
return showLoginPopup();
|
return showLoginPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasExceededLimit =
|
||||||
|
!isPaidUser &&
|
||||||
|
existingRoadmapCount > 0 &&
|
||||||
|
existingRoadmapCount >= MAX_ROADMAP_LIMIT;
|
||||||
|
|
||||||
|
if (hasExceededLimit) {
|
||||||
|
setShowUpgradeModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsCreatingRoadmap(true);
|
setIsCreatingRoadmap(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{showUpgradeModal && (
|
||||||
|
<UpgradeAccountModal onClose={() => setShowUpgradeModal(false)} />
|
||||||
|
)}
|
||||||
|
|
||||||
{isCreatingRoadmap && (
|
{isCreatingRoadmap && (
|
||||||
<CreateRoadmapModal
|
<CreateRoadmapModal
|
||||||
teamId={teamId}
|
teamId={teamId}
|
||||||
|
@@ -39,7 +39,7 @@ const tabTypes: TabType[] = [
|
|||||||
{ label: 'Shared by Friends', value: 'shared' },
|
{ label: 'Shared by Friends', value: 'shared' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const MAX_ROADMAP_LIMIT = 3;
|
export const MAX_ROADMAP_LIMIT = 3;
|
||||||
|
|
||||||
export function RoadmapListPage() {
|
export function RoadmapListPage() {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
@@ -1,12 +1,4 @@
|
|||||||
import { useStore } from '@nanostores/react';
|
import { CheckSquare, Gift, SquarePen } from 'lucide-react';
|
||||||
import {
|
|
||||||
ChartColumn,
|
|
||||||
CheckSquare,
|
|
||||||
FolderGit2,
|
|
||||||
SquarePen,
|
|
||||||
Zap,
|
|
||||||
type LucideIcon
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import type { AllowedProfileVisibility } from '../../api/user.ts';
|
import type { AllowedProfileVisibility } from '../../api/user.ts';
|
||||||
import { useToast } from '../../hooks/use-toast';
|
import { useToast } from '../../hooks/use-toast';
|
||||||
@@ -16,7 +8,7 @@ import { httpGet } from '../../lib/http';
|
|||||||
import type { QuestionGroupType } from '../../lib/question-group';
|
import type { QuestionGroupType } from '../../lib/question-group';
|
||||||
import type { AllowedRoadmapRenderer } from '../../lib/roadmap.ts';
|
import type { AllowedRoadmapRenderer } from '../../lib/roadmap.ts';
|
||||||
import type { VideoFileType } from '../../lib/video';
|
import type { VideoFileType } from '../../lib/video';
|
||||||
import { $accountStreak, type StreakResponse } from '../../stores/streak';
|
import { type StreakResponse } from '../../stores/streak';
|
||||||
import type { PageType } from '../CommandMenu/CommandMenu';
|
import type { PageType } from '../CommandMenu/CommandMenu';
|
||||||
import { FeaturedGuideList } from '../FeaturedGuides/FeaturedGuideList';
|
import { FeaturedGuideList } from '../FeaturedGuides/FeaturedGuideList';
|
||||||
import { FeaturedVideoList } from '../FeaturedVideos/FeaturedVideoList';
|
import { FeaturedVideoList } from '../FeaturedVideos/FeaturedVideoList';
|
||||||
@@ -27,8 +19,10 @@ import {
|
|||||||
import { HeroRoadmap } from '../HeroSection/HeroRoadmap.tsx';
|
import { HeroRoadmap } from '../HeroSection/HeroRoadmap.tsx';
|
||||||
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions';
|
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions';
|
||||||
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
|
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
|
||||||
|
import { useIsPaidUser } from '../../queries/billing.ts';
|
||||||
|
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal.tsx';
|
||||||
|
|
||||||
const projectGroups = [
|
const projectGroups = [
|
||||||
{
|
{
|
||||||
title: 'Frontend',
|
title: 'Frontend',
|
||||||
id: 'frontend',
|
id: 'frontend',
|
||||||
@@ -53,7 +47,6 @@ type UserDashboardResponse = {
|
|||||||
progresses: UserProgress[];
|
progresses: UserProgress[];
|
||||||
projects: ProjectStatusDocument[];
|
projects: ProjectStatusDocument[];
|
||||||
aiRoadmaps: AIRoadmapType[];
|
aiRoadmaps: AIRoadmapType[];
|
||||||
topicDoneToday: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BuiltInRoadmap = {
|
export type BuiltInRoadmap = {
|
||||||
@@ -78,34 +71,40 @@ type PersonalDashboardProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type DashboardStatItemProps = {
|
type DashboardStatItemProps = {
|
||||||
icon: LucideIcon;
|
|
||||||
iconClassName: string;
|
|
||||||
value: number;
|
|
||||||
label: string;
|
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function DashboardStatItem(props: DashboardStatItemProps) {
|
function UpgradeAccountButton(props: DashboardStatItemProps) {
|
||||||
const { icon: Icon, iconClassName, value, label, isLoading } = props;
|
const { isLoading } = props;
|
||||||
|
|
||||||
|
const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
|
||||||
|
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
|
||||||
|
|
||||||
|
if (isPaidUser) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
className={cn(
|
{showUpgradeModal && (
|
||||||
'flex items-center gap-1.5 rounded-lg bg-slate-800/50 py-2 pl-3 pr-3',
|
<UpgradeAccountModal onClose={() => setShowUpgradeModal(false)} />
|
||||||
{
|
|
||||||
'striped-loader-slate striped-loader-slate-fast text-transparent':
|
|
||||||
isLoading,
|
|
||||||
},
|
|
||||||
)}
|
)}
|
||||||
>
|
|
||||||
<Icon
|
<button
|
||||||
size={16}
|
onClickCapture={() => setShowUpgradeModal(true)}
|
||||||
className={cn(iconClassName, { 'text-transparent': isLoading })}
|
className={cn(
|
||||||
/>
|
'flex items-center gap-1.5 rounded-lg bg-white py-1.5 pr-3 pl-2.5 text-xs text-purple-700 duration-200 hover:bg-purple-600 hover:text-white',
|
||||||
<span>
|
{
|
||||||
<span className="tabular-nums">{value}</span> {label}
|
'striped-loader-slate striped-loader-slate-fast border-transparent bg-slate-800/50 text-transparent hover:bg-transparent hover:text-transparent hover:shadow-none':
|
||||||
</span>
|
isLoading || isPaidUserLoading,
|
||||||
</div>
|
},
|
||||||
|
)}
|
||||||
|
onClick={() => {}}
|
||||||
|
>
|
||||||
|
<Gift size={16} className={cn({ 'text-transparent': isLoading })} />
|
||||||
|
<span>Upgrade Account</span>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +123,7 @@ function PersonalProfileButton(props: ProfileButtonProps) {
|
|||||||
<a
|
<a
|
||||||
href="/account/update-profile"
|
href="/account/update-profile"
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-2 rounded-lg bg-slate-800/50 py-2 pl-3 pr-3 font-medium outline-slate-700 hover:bg-slate-800 hover:outline-slate-400',
|
'flex items-center gap-2 rounded-lg bg-slate-800/50 py-2 pr-3 pl-3 font-medium outline-slate-700 hover:bg-slate-800 hover:outline-slate-400',
|
||||||
{
|
{
|
||||||
'striped-loader-slate striped-loader-slate-fast text-transparent':
|
'striped-loader-slate striped-loader-slate-fast text-transparent':
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -142,7 +141,7 @@ function PersonalProfileButton(props: ProfileButtonProps) {
|
|||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5">
|
||||||
<a
|
<a
|
||||||
href={`/u/${username}`}
|
href={`/u/${username}`}
|
||||||
className="flex items-center gap-2 rounded-lg bg-slate-800/50 py-2 pl-3 pr-3 text-slate-300 transition-colors hover:bg-slate-800/70"
|
className="flex items-center gap-2 rounded-lg bg-slate-800/50 py-2 pr-3 pl-3 text-slate-300 transition-colors hover:bg-slate-800/70"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={avatar}
|
src={avatar}
|
||||||
@@ -153,7 +152,7 @@ function PersonalProfileButton(props: ProfileButtonProps) {
|
|||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="/account/update-profile"
|
href="/account/update-profile"
|
||||||
className="flex items-center gap-2 rounded-lg bg-slate-800/50 py-2 pl-3 pr-3 text-slate-400 transition-colors hover:bg-slate-800/70 hover:text-slate-300"
|
className="flex items-center gap-2 rounded-lg bg-slate-800/50 py-2 pr-3 pl-3 text-slate-400 transition-colors hover:bg-slate-800/70 hover:text-slate-300"
|
||||||
title="Edit Profile"
|
title="Edit Profile"
|
||||||
>
|
>
|
||||||
<SquarePen className="h-4 w-4" />
|
<SquarePen className="h-4 w-4" />
|
||||||
@@ -165,22 +164,15 @@ function PersonalProfileButton(props: ProfileButtonProps) {
|
|||||||
type DashboardStatsProps = {
|
type DashboardStatsProps = {
|
||||||
profile: ProfileButtonProps;
|
profile: ProfileButtonProps;
|
||||||
accountStreak?: StreakResponse;
|
accountStreak?: StreakResponse;
|
||||||
topicsDoneToday?: number;
|
|
||||||
finishedProjectsCount?: number;
|
finishedProjectsCount?: number;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function DashboardStats(props: DashboardStatsProps) {
|
function DashboardStats(props: DashboardStatsProps) {
|
||||||
const {
|
const { isLoading, profile } = props;
|
||||||
accountStreak,
|
|
||||||
topicsDoneToday = 0,
|
|
||||||
finishedProjectsCount = 0,
|
|
||||||
isLoading,
|
|
||||||
profile,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mb-3 flex flex-col gap-4 pb-2 pt-6 text-sm text-slate-400 sm:flex-row sm:items-center sm:justify-between">
|
<div className="container mb-3 flex flex-col gap-4 pt-6 pb-2 text-sm text-slate-400 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex w-full flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex w-full flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<PersonalProfileButton
|
<PersonalProfileButton
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
@@ -189,27 +181,7 @@ function DashboardStats(props: DashboardStatsProps) {
|
|||||||
avatar={profile.avatar}
|
avatar={profile.avatar}
|
||||||
/>
|
/>
|
||||||
<div className="hidden flex-wrap items-center gap-2 md:flex">
|
<div className="hidden flex-wrap items-center gap-2 md:flex">
|
||||||
<DashboardStatItem
|
<UpgradeAccountButton isLoading={isLoading} />
|
||||||
icon={Zap}
|
|
||||||
iconClassName="text-yellow-500"
|
|
||||||
value={accountStreak?.count || 0}
|
|
||||||
label="day streak"
|
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
|
||||||
<DashboardStatItem
|
|
||||||
icon={ChartColumn}
|
|
||||||
iconClassName="text-green-500"
|
|
||||||
value={topicsDoneToday}
|
|
||||||
label="learnt today"
|
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
|
||||||
<DashboardStatItem
|
|
||||||
icon={FolderGit2}
|
|
||||||
iconClassName="text-blue-500"
|
|
||||||
value={finishedProjectsCount}
|
|
||||||
label="projects finished"
|
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -232,25 +204,6 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
|
|||||||
const [personalDashboardDetails, setPersonalDashboardDetails] =
|
const [personalDashboardDetails, setPersonalDashboardDetails] =
|
||||||
useState<UserDashboardResponse>();
|
useState<UserDashboardResponse>();
|
||||||
const [projectDetails, setProjectDetails] = useState<PageType[]>([]);
|
const [projectDetails, setProjectDetails] = useState<PageType[]>([]);
|
||||||
const accountStreak = useStore($accountStreak);
|
|
||||||
|
|
||||||
const loadAccountStreak = async () => {
|
|
||||||
if (accountStreak) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
const { response, error } = await httpGet<StreakResponse>(
|
|
||||||
`${import.meta.env.PUBLIC_API_URL}/v1-streak`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error || !response) {
|
|
||||||
toast.error(error?.message || 'Failed to load account streak');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$accountStreak.set(response);
|
|
||||||
};
|
|
||||||
|
|
||||||
async function loadProgress() {
|
async function loadProgress() {
|
||||||
const { response: progressList, error } =
|
const { response: progressList, error } =
|
||||||
@@ -293,11 +246,9 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.allSettled([
|
Promise.allSettled([loadProgress(), loadAllProjectDetails()]).finally(() =>
|
||||||
loadProgress(),
|
setIsLoading(false),
|
||||||
loadAllProjectDetails(),
|
);
|
||||||
loadAccountStreak(),
|
|
||||||
]).finally(() => setIsLoading(false));
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -324,8 +275,6 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
|
|||||||
return updatedAtB.getTime() - updatedAtA.getTime();
|
return updatedAtB.getTime() - updatedAtA.getTime();
|
||||||
});
|
});
|
||||||
|
|
||||||
const aiGeneratedRoadmaps = personalDashboardDetails?.aiRoadmaps || [];
|
|
||||||
|
|
||||||
const customRoadmaps: UserProgress[] = (
|
const customRoadmaps: UserProgress[] = (
|
||||||
personalDashboardDetails?.progresses || []
|
personalDashboardDetails?.progresses || []
|
||||||
)
|
)
|
||||||
@@ -376,18 +325,11 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
|
|||||||
isLoading,
|
isLoading,
|
||||||
}}
|
}}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
accountStreak={accountStreak}
|
|
||||||
topicsDoneToday={personalDashboardDetails?.topicDoneToday}
|
|
||||||
finishedProjectsCount={
|
|
||||||
enrichedProjects?.filter((p) => p.submittedAt && p.repositoryUrl)
|
|
||||||
.length
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FavoriteRoadmaps
|
<FavoriteRoadmaps
|
||||||
progress={learningRoadmapsToShow}
|
progress={learningRoadmapsToShow}
|
||||||
customRoadmaps={customRoadmaps}
|
customRoadmaps={customRoadmaps}
|
||||||
aiRoadmaps={aiGeneratedRoadmaps}
|
|
||||||
projects={enrichedProjects || []}
|
projects={enrichedProjects || []}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
|
@@ -2,9 +2,9 @@ import {
|
|||||||
FolderKanban,
|
FolderKanban,
|
||||||
MapIcon,
|
MapIcon,
|
||||||
Plus,
|
Plus,
|
||||||
Sparkle,
|
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff, SquareCheckBig
|
EyeOff,
|
||||||
|
SquareCheckBig,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions.tsx';
|
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions.tsx';
|
||||||
@@ -28,12 +28,11 @@ type FavoriteRoadmapsProps = {
|
|||||||
title: string;
|
title: string;
|
||||||
})[];
|
})[];
|
||||||
customRoadmaps: UserProgress[];
|
customRoadmaps: UserProgress[];
|
||||||
aiRoadmaps: AIRoadmapType[];
|
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function FavoriteRoadmaps(props: FavoriteRoadmapsProps) {
|
export function FavoriteRoadmaps(props: FavoriteRoadmapsProps) {
|
||||||
const { progress, isLoading, customRoadmaps, aiRoadmaps, projects } = props;
|
const { progress, isLoading, customRoadmaps, projects } = props;
|
||||||
const [showCompleted, setShowCompleted] = useState(false);
|
const [showCompleted, setShowCompleted] = useState(false);
|
||||||
const [isCreatingCustomRoadmap, setIsCreatingCustomRoadmap] = useState(false);
|
const [isCreatingCustomRoadmap, setIsCreatingCustomRoadmap] = useState(false);
|
||||||
|
|
||||||
@@ -131,49 +130,7 @@ export function FavoriteRoadmaps(props: FavoriteRoadmapsProps) {
|
|||||||
allowFavorite={false}
|
allowFavorite={false}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<CreateRoadmapButton />
|
<CreateRoadmapButton existingRoadmapCount={customRoadmaps.length} />
|
||||||
</HeroItemsGroup>
|
|
||||||
|
|
||||||
<HeroItemsGroup
|
|
||||||
icon={<Sparkle className="mr-1.5 h-[14px] w-[14px]" />}
|
|
||||||
isLoading={isLoading}
|
|
||||||
title="Your AI roadmaps"
|
|
||||||
isEmpty={!isLoading && aiRoadmaps.length === 0}
|
|
||||||
emptyTitle={
|
|
||||||
<>
|
|
||||||
No AI roadmaps found
|
|
||||||
<a
|
|
||||||
href="/ai-roadmaps"
|
|
||||||
className="ml-1.5 inline-flex items-center gap-1 font-medium text-blue-500 underline-offset-2 hover:underline"
|
|
||||||
>
|
|
||||||
<SquareCheckBig className="size-3.5" strokeWidth={2.5} />
|
|
||||||
Generate AI roadmap
|
|
||||||
</a>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{aiRoadmaps.map((aiRoadmap) => (
|
|
||||||
<HeroRoadmap
|
|
||||||
key={aiRoadmap.id}
|
|
||||||
resourceId={aiRoadmap.id}
|
|
||||||
resourceType={'roadmap'}
|
|
||||||
resourceTitle={aiRoadmap.title}
|
|
||||||
url={`/ai-roadmaps/${aiRoadmap.slug}`}
|
|
||||||
percentageDone={0}
|
|
||||||
allowFavorite={false}
|
|
||||||
isTrackable={false}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="/ai-roadmaps"
|
|
||||||
className={
|
|
||||||
'flex h-full w-full items-center justify-center gap-1 overflow-hidden rounded-md border border-dashed border-gray-800 p-3 text-sm text-gray-400 hover:border-gray-600 hover:bg-gray-900 hover:text-gray-300'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Plus size={16} />
|
|
||||||
Generate New
|
|
||||||
</a>
|
|
||||||
</HeroItemsGroup>
|
</HeroItemsGroup>
|
||||||
|
|
||||||
<HeroItemsGroup
|
<HeroItemsGroup
|
||||||
|
Reference in New Issue
Block a user