feat: init
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
chuan
2025-11-11 01:56:44 +08:00
commit bba4bb40c8
4638 changed files with 447437 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
"use client";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
// ui
import { useTranslation } from "@plane/i18n";
import { Loader, Card } from "@plane/ui";
import { calculateTimeAgo, getFileURL } from "@plane/utils";
// components
import { ActivityMessage, IssueLink } from "@/components/core/activity";
import { ProfileEmptyState } from "@/components/ui/profile-empty-state";
// constants
import { USER_PROFILE_ACTIVITY } from "@/constants/fetch-keys";
// helpers
// hooks
import { useUser } from "@/hooks/store/user";
// assets
import recentActivityEmptyState from "@/public/empty-state/recent_activity.svg";
// services
import { UserService } from "@/services/user.service";
const userService = new UserService();
export const ProfileActivity = observer(() => {
const { workspaceSlug, userId } = useParams();
// store hooks
const { data: currentUser } = useUser();
const { t } = useTranslation();
const { data: userProfileActivity } = useSWR(
workspaceSlug && userId ? USER_PROFILE_ACTIVITY(workspaceSlug.toString(), userId.toString(), {}) : null,
workspaceSlug && userId
? () =>
userService.getUserProfileActivity(workspaceSlug.toString(), userId.toString(), {
per_page: 10,
})
: null
);
return (
<div className="space-y-2">
<h3 className="text-lg font-medium">{t("profile.stats.recent_activity.title")}</h3>
<Card>
{userProfileActivity ? (
userProfileActivity.results.length > 0 ? (
<div className="space-y-5">
{userProfileActivity.results.map((activity) => (
<div key={activity.id} className="flex gap-3">
<div className="flex-shrink-0 grid place-items-center overflow-hidden rounded h-6 w-6">
{activity.actor_detail?.avatar_url && activity.actor_detail?.avatar_url !== "" ? (
<img
src={getFileURL(activity.actor_detail?.avatar_url)}
alt={activity.actor_detail?.display_name}
className="rounded"
/>
) : (
<div className="grid h-6 w-6 place-items-center rounded border-2 bg-gray-700 text-xs text-white">
{activity.actor_detail?.display_name?.charAt(0)}
</div>
)}
</div>
<div className="-mt-1 w-4/5 break-words">
<p className="inline text-sm text-custom-text-200">
<span className="font-medium text-custom-text-100">
{currentUser?.id === activity.actor_detail?.id
? "You"
: activity.actor_detail?.display_name}{" "}
</span>
{activity.field ? (
<ActivityMessage activity={activity} showIssue />
) : (
<span>
created <IssueLink activity={activity} />
</span>
)}
</p>
<p className="text-xs text-custom-text-200 whitespace-nowrap ">
{calculateTimeAgo(activity.created_at)}
</p>
</div>
</div>
))}
</div>
) : (
<ProfileEmptyState
title={t("no_data_yet")}
description={t("profile.stats.recent_activity.empty")}
image={recentActivityEmptyState}
/>
)
) : (
<Loader className="space-y-5">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
)}
</Card>
</div>
);
});

View File

@@ -0,0 +1,87 @@
"use client";
// plane imports
import { useTranslation } from "@plane/i18n";
import { BarChart } from "@plane/propel/charts/bar-chart";
import type { IUserProfileData } from "@plane/types";
import { Loader, Card } from "@plane/ui";
import { capitalizeFirstLetter } from "@plane/utils";
// components
import { ProfileEmptyState } from "@/components/ui/profile-empty-state";
// assets
import emptyBarGraph from "@/public/empty-state/empty_bar_graph.svg";
type Props = {
userProfile: IUserProfileData | undefined;
};
const priorityColors = {
urgent: "#991b1b",
high: "#ef4444",
medium: "#f59e0b",
low: "#16a34a",
none: "#e5e5e5",
};
export const ProfilePriorityDistribution: React.FC<Props> = ({ userProfile }) => {
const { t } = useTranslation();
return (
<div className="flex flex-col space-y-2">
<h3 className="text-lg font-medium">{t("profile.stats.priority_distribution.title")}</h3>
{userProfile ? (
<Card>
{userProfile.priority_distribution.length > 0 ? (
<BarChart
className="w-full h-[300px]"
margin={{ top: 20, right: 30, bottom: 5, left: 0 }}
data={userProfile.priority_distribution.map((priority) => ({
key: priority.priority ?? "None",
name: capitalizeFirstLetter(priority.priority ?? "None"),
count: priority.priority_count,
}))}
bars={[
{
key: "count",
label: "Count",
stackId: "bar-one",
fill: (payload: any) => priorityColors[payload.key as keyof typeof priorityColors], // TODO: fix types
textClassName: "",
showPercentage: false,
showTopBorderRadius: () => true,
showBottomBorderRadius: () => true,
},
]}
xAxis={{
key: "name",
label: t("profile.stats.priority_distribution.priority"),
}}
yAxis={{
key: "count",
label: "",
}}
barSize={20}
/>
) : (
<div className="flex-grow p-7">
<ProfileEmptyState
title={t("no_data_yet")}
description={t("profile.stats.priority_distribution.empty")}
image={emptyBarGraph}
/>
</div>
)}
</Card>
) : (
<div className="grid place-items-center p-7">
<Loader className="flex items-end gap-12">
<Loader.Item width="30px" height="200px" />
<Loader.Item width="30px" height="150px" />
<Loader.Item width="30px" height="250px" />
<Loader.Item width="30px" height="150px" />
<Loader.Item width="30px" height="100px" />
</Loader>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,86 @@
// plane imports
import { STATE_GROUPS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { PieChart } from "@plane/propel/charts/pie-chart";
import type { IUserProfileData, IUserStateDistribution } from "@plane/types";
import { Card } from "@plane/ui";
import { capitalizeFirstLetter } from "@plane/utils";
// components
import { ProfileEmptyState } from "@/components/ui/profile-empty-state";
// assets
import stateGraph from "@/public/empty-state/state_graph.svg";
type Props = {
stateDistribution: IUserStateDistribution[];
userProfile: IUserProfileData | undefined;
};
export const ProfileStateDistribution: React.FC<Props> = ({ stateDistribution, userProfile }) => {
const { t } = useTranslation();
if (!userProfile) return null;
return (
<div className="flex flex-col space-y-2">
<h3 className="text-lg font-medium">{t("profile.stats.state_distribution.title")}</h3>
<Card className="h-full">
{userProfile.state_distribution.length > 0 ? (
<div className="grid grid-cols-1 gap-x-6 md:grid-cols-2 w-full h-[300px]">
<PieChart
className="size-full"
dataKey="value"
margin={{
top: 0,
right: -10,
bottom: 12,
left: -10,
}}
data={
userProfile.state_distribution.map((group) => ({
id: group.state_group,
key: group.state_group,
value: group.state_count,
name: capitalizeFirstLetter(group.state_group),
color: STATE_GROUPS[group.state_group]?.color,
})) ?? []
}
cells={userProfile.state_distribution.map((group) => ({
key: group.state_group,
fill: STATE_GROUPS[group.state_group]?.color,
}))}
showTooltip
tooltipLabel="Count"
paddingAngle={5}
cornerRadius={4}
innerRadius="50%"
showLabel={false}
/>
<div className="flex items-center">
<div className="w-full space-y-4">
{stateDistribution.map((group) => (
<div key={group.state_group} className="flex items-center justify-between gap-2 text-xs">
<div className="flex items-center gap-1.5">
<div
className="h-2.5 w-2.5 rounded-sm"
style={{
backgroundColor: STATE_GROUPS[group.state_group]?.color ?? "rgb(var(--color-primary-100))",
}}
/>
<div className="whitespace-nowrap">{STATE_GROUPS[group.state_group].label}</div>
</div>
<div>{group.state_count}</div>
</div>
))}
</div>
</div>
</div>
) : (
<ProfileEmptyState
title={t("no_data_yet")}
description={t("profile.stats.state_distribution.empty")}
image={stateGraph}
/>
)}
</Card>
</div>
);
};

View File

@@ -0,0 +1,72 @@
"use client";
import Link from "next/link";
import { useParams } from "next/navigation";
// ui
import { UserCircle2 } from "lucide-react";
import { useTranslation } from "@plane/i18n";
import { CreateIcon, LayerStackIcon } from "@plane/propel/icons";
import type { IUserProfileData } from "@plane/types";
import { Loader, Card, ECardSpacing, ECardDirection } from "@plane/ui";
// types
type Props = {
userProfile: IUserProfileData | undefined;
};
export const ProfileStats: React.FC<Props> = ({ userProfile }) => {
const { workspaceSlug, userId } = useParams();
const { t } = useTranslation();
const overviewCards = [
{
icon: CreateIcon,
route: "created",
i18n_title: "profile.stats.created",
value: userProfile?.created_issues ?? "...",
},
{
icon: UserCircle2,
route: "assigned",
i18n_title: "profile.stats.assigned",
value: userProfile?.assigned_issues ?? "...",
},
{
icon: LayerStackIcon,
route: "subscribed",
i18n_title: "profile.stats.subscribed",
value: userProfile?.subscribed_issues ?? "...",
},
];
return (
<div className="space-y-2">
<h3 className="text-lg font-medium">{t("profile.stats.overview")}</h3>
{userProfile ? (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{overviewCards.map((card) => (
<Link key={card.route} href={`/${workspaceSlug}/profile/${userId}/${card.route}`}>
<Card direction={ECardDirection.ROW} spacing={ECardSpacing.SM} className="h-full">
<div className="grid h-11 w-11 place-items-center rounded bg-custom-background-90">
<card.icon className="h-5 w-5" />
</div>
<div className="space-y-1">
<p className="text-sm text-custom-text-400">{t(card.i18n_title)}</p>
<p className="text-xl font-semibold">{card.value}</p>
</div>
</Card>
</Link>
))}
</div>
) : (
<Loader className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<Loader.Item height="80px" />
<Loader.Item height="80px" />
<Loader.Item height="80px" />
</Loader>
)}
</div>
);
};

View File

@@ -0,0 +1,47 @@
// plane imports
import { STATE_GROUPS } from "@plane/constants";
// types
import { useTranslation } from "@plane/i18n";
import type { IUserStateDistribution } from "@plane/types";
import { Card, ECardDirection, ECardSpacing } from "@plane/ui";
// constants
type Props = {
stateDistribution: IUserStateDistribution[];
};
export const ProfileWorkload: React.FC<Props> = ({ stateDistribution }) => {
const { t } = useTranslation();
return (
<div className="space-y-2">
<h3 className="text-lg font-medium">{t("profile.stats.workload")}</h3>
<div className="grid grid-cols-1 justify-stretch gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
{stateDistribution.map((group) => (
<div key={group.state_group}>
<a>
<Card direction={ECardDirection.ROW} spacing={ECardSpacing.SM}>
<div
className="h-3 w-3 rounded-sm my-2"
style={{
backgroundColor: STATE_GROUPS[group.state_group].color,
}}
/>
<div className="space-y-1 flex-col">
<span className="text-sm text-custom-text-400">
{group.state_group === "unstarted"
? "Not started"
: group.state_group === "started"
? "Working on"
: STATE_GROUPS[group.state_group].label}
</span>
<p className="text-xl font-semibold">{group.state_count}</p>
</div>
</Card>
</a>
</div>
))}
</div>
</div>
);
};