feat: website detail basic page
This commit is contained in:
parent
3d9f768a44
commit
b5708b241a
@ -1,6 +1,7 @@
|
|||||||
|
import { Alert } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export const ErrorTip: React.FC = React.memo(() => {
|
export const ErrorTip: React.FC = React.memo(() => {
|
||||||
return <div>An unexpected error has occurred</div>;
|
return <Alert message="An unexpected error has occurred" type="error" />;
|
||||||
});
|
});
|
||||||
ErrorTip.displayName = 'ErrorTip';
|
ErrorTip.displayName = 'ErrorTip';
|
||||||
|
7
src/client/components/NotFoundTip.tsx
Normal file
7
src/client/components/NotFoundTip.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { Alert } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const NotFoundTip: React.FC = React.memo(() => {
|
||||||
|
return <Alert message="Not found" type="error" />;
|
||||||
|
});
|
||||||
|
NotFoundTip.displayName = 'NotFoundTip';
|
@ -119,7 +119,14 @@ const WebsiteListTable: React.FC<{ workspaceId: string }> = React.memo(
|
|||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
<Button icon={<BarChartOutlined />}>View</Button>
|
<Button
|
||||||
|
icon={<BarChartOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
navigate(`/website/${record.id}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import { Button, message, Tag } from 'antd';
|
import { Button, message } from 'antd';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Column, ColumnConfig } from '@ant-design/charts';
|
import { Column, ColumnConfig } from '@ant-design/charts';
|
||||||
import { ArrowRightOutlined, SyncOutlined } from '@ant-design/icons';
|
import { SyncOutlined } from '@ant-design/icons';
|
||||||
import { DateFilter } from './DateFilter';
|
import { DateFilter } from './DateFilter';
|
||||||
import { HealthBar } from './HealthBar';
|
import { HealthBar } from './HealthBar';
|
||||||
import {
|
import {
|
||||||
StatsItemType,
|
StatsItemType,
|
||||||
useWorkspaceWebsitePageview,
|
useWorkspaceWebsitePageview,
|
||||||
useWorkspaceWebsiteStats,
|
useWorkspaceWebsiteStats,
|
||||||
useWorspaceWebsites,
|
|
||||||
WebsiteInfo,
|
WebsiteInfo,
|
||||||
} from '../api/model/website';
|
} from '../api/model/website';
|
||||||
import { Loading } from './Loading';
|
import { Loading } from './Loading';
|
||||||
@ -25,30 +24,9 @@ import { formatNumber, formatShortTime } from '../utils/common';
|
|||||||
import { useTheme } from '../hooks/useTheme';
|
import { useTheme } from '../hooks/useTheme';
|
||||||
import { WebsiteOnlineCount } from './WebsiteOnlineCount';
|
import { WebsiteOnlineCount } from './WebsiteOnlineCount';
|
||||||
|
|
||||||
interface WebsiteOverviewProps {
|
export const WebsiteOverview: React.FC<{
|
||||||
workspaceId: string;
|
|
||||||
}
|
|
||||||
export const WebsiteOverview: React.FC<WebsiteOverviewProps> = React.memo(
|
|
||||||
(props) => {
|
|
||||||
const { isLoading, websites } = useWorspaceWebsites(props.workspaceId);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <Loading />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{websites.map((website) => (
|
|
||||||
<WebsiteOverviewItem key={website.id} website={website} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
WebsiteOverview.displayName = 'WebsiteOverview';
|
|
||||||
|
|
||||||
const WebsiteOverviewItem: React.FC<{
|
|
||||||
website: WebsiteInfo;
|
website: WebsiteInfo;
|
||||||
|
actions?: React.ReactNode;
|
||||||
}> = React.memo((props) => {
|
}> = React.memo((props) => {
|
||||||
const unit: DateUnit = 'hour';
|
const unit: DateUnit = 'hour';
|
||||||
const startDate = dayjs().subtract(1, 'day').add(1, unit).startOf(unit);
|
const startDate = dayjs().subtract(1, 'day').add(1, unit).startOf(unit);
|
||||||
@ -99,7 +77,7 @@ const WebsiteOverviewItem: React.FC<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-10 pb-10 border-b">
|
<div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex flex-1 text-2xl font-bold items-center">
|
<div className="flex flex-1 text-2xl font-bold items-center">
|
||||||
<span className="mr-2" title={props.website.domain ?? ''}>
|
<span className="mr-2" title={props.website.domain ?? ''}>
|
||||||
@ -120,11 +98,7 @@ const WebsiteOverviewItem: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>{props.actions}</div>
|
||||||
<Button type="primary" size="large">
|
|
||||||
View Details <ArrowRightOutlined />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex mb-10 flex-wrap">
|
<div className="flex mb-10 flex-wrap">
|
||||||
@ -147,7 +121,7 @@ const WebsiteOverviewItem: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
WebsiteOverviewItem.displayName = 'WebsiteOverviewItem';
|
WebsiteOverview.displayName = 'WebsiteOverview';
|
||||||
|
|
||||||
export const MetricsBar: React.FC<{
|
export const MetricsBar: React.FC<{
|
||||||
stats: {
|
stats: {
|
||||||
|
@ -1,14 +1,23 @@
|
|||||||
import React from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { EditOutlined } from '@ant-design/icons';
|
import { ArrowRightOutlined, EditOutlined } from '@ant-design/icons';
|
||||||
import { Button } from 'antd';
|
import { Button, Divider } from 'antd';
|
||||||
import { WebsiteOverview } from '../components/WebsiteOverview';
|
import { WebsiteOverview } from '../components/WebsiteOverview';
|
||||||
import { useCurrentWorkspaceId } from '../store/user';
|
import { useCurrentWorkspaceId } from '../store/user';
|
||||||
import { Loading } from '../components/Loading';
|
import { Loading } from '../components/Loading';
|
||||||
|
import { useWorspaceWebsites } from '../api/model/website';
|
||||||
|
import { NoWorkspaceTip } from '../components/NoWorkspaceTip';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
export const Dashboard: React.FC = React.memo(() => {
|
export const Dashboard: React.FC = React.memo(() => {
|
||||||
const workspaceId = useCurrentWorkspaceId();
|
const workspaceId = useCurrentWorkspaceId()!;
|
||||||
|
const { isLoading, websites } = useWorspaceWebsites(workspaceId);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
if (!workspaceId) {
|
if (!workspaceId) {
|
||||||
|
return <NoWorkspaceTip />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,7 +32,28 @@ export const Dashboard: React.FC = React.memo(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<WebsiteOverview workspaceId={workspaceId} />
|
{websites.map((website, i) => (
|
||||||
|
<Fragment key={website.id}>
|
||||||
|
{i !== 0 && <Divider />}
|
||||||
|
|
||||||
|
<WebsiteOverview
|
||||||
|
website={website}
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
onClick={() => {
|
||||||
|
navigate(`/website/${website.id}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View Details <ArrowRightOutlined />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
47
src/client/pages/Website/Detail.tsx
Normal file
47
src/client/pages/Website/Detail.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Divider } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import { useParams } from 'react-router';
|
||||||
|
import { trpc } from '../../api/trpc';
|
||||||
|
import { ErrorTip } from '../../components/ErrorTip';
|
||||||
|
import { Loading } from '../../components/Loading';
|
||||||
|
import { NotFoundTip } from '../../components/NotFoundTip';
|
||||||
|
import { WebsiteOverview } from '../../components/WebsiteOverview';
|
||||||
|
import { useCurrentWorkspaceId } from '../../store/user';
|
||||||
|
|
||||||
|
export const WebsiteDetail: React.FC = React.memo(() => {
|
||||||
|
const { websiteId } = useParams();
|
||||||
|
const workspaceId = useCurrentWorkspaceId();
|
||||||
|
const { data: website, isLoading } = trpc.website.info.useQuery({
|
||||||
|
workspaceId,
|
||||||
|
websiteId: websiteId!,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!websiteId) {
|
||||||
|
return <ErrorTip />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!website) {
|
||||||
|
return <NotFoundTip />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="py-6">
|
||||||
|
<div>
|
||||||
|
<WebsiteOverview website={website} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex-1">left</div>
|
||||||
|
<Divider type="vertical" />
|
||||||
|
<div className="flex-1">right</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
WebsiteDetail.displayName = 'WebsiteDetail';
|
@ -1,12 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Route, Routes } from 'react-router';
|
import { Route, Routes } from 'react-router';
|
||||||
import { WebsiteList } from '../../components/WebsiteList';
|
import { WebsiteList } from '../../components/WebsiteList';
|
||||||
|
import { WebsiteDetail } from './Detail';
|
||||||
|
|
||||||
export const WebsitePage: React.FC = React.memo(() => {
|
export const WebsitePage: React.FC = React.memo(() => {
|
||||||
return (
|
return (
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<WebsiteList />} />
|
<Route path="/" element={<WebsiteList />} />
|
||||||
|
<Route path="/:websiteId" element={<WebsiteDetail />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { router, workspaceProcedure } from '../trpc';
|
import { router, workspaceProcedure } from '../trpc';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { getWebsiteOnlineUserCount } from '../../model/website';
|
import { getWebsiteOnlineUserCount } from '../../model/website';
|
||||||
|
import { prisma } from '../../model/_client';
|
||||||
|
|
||||||
export const websiteRouter = router({
|
export const websiteRouter = router({
|
||||||
onlineCount: workspaceProcedure
|
onlineCount: workspaceProcedure
|
||||||
@ -16,4 +17,21 @@ export const websiteRouter = router({
|
|||||||
|
|
||||||
return count;
|
return count;
|
||||||
}),
|
}),
|
||||||
|
info: workspaceProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
websiteId: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
const { websiteId, workspaceId } = input;
|
||||||
|
|
||||||
|
const website = await prisma.website.findUnique({
|
||||||
|
where: {
|
||||||
|
id: websiteId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return website;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user