Skip to content

Commit

Permalink
Merge pull request #121 from times-yasunori/stats
Browse files Browse the repository at this point in the history
Awesome Yasunori Stats
  • Loading branch information
tomoya authored Oct 15, 2024
2 parents eeefbca + 158a74d commit 9623c5d
Show file tree
Hide file tree
Showing 6 changed files with 428 additions and 23 deletions.
8 changes: 7 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
},
"files": {
"ignoreUnknown": false,
"ignore": ["build", "dist", "awesome-yasunori.json", ".wrangler"]
"ignore": [
"build",
"dist",
"awesome-yasunori.json",
".wrangler",
"cljs-yasunori-block"
]
},
"formatter": {
"enabled": true,
Expand Down
74 changes: 52 additions & 22 deletions packages/web/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import {
ActionIcon,
AppShell,
Burger,
Button,
ColorSchemeScript,
Group,
MantineProvider,
NavLink,
ScrollArea,
Text,
Title,
em,
rem,
} from "@mantine/core";
import type { LinksFunction } from "@remix-run/cloudflare";
Expand All @@ -18,12 +20,14 @@ import {
Outlet,
Scripts,
ScrollRestoration,
useMatches,
useNavigate,
useRouteLoaderData,
} from "@remix-run/react";
import "@mantine/core/styles.css";
import { useDisclosure, useHeadroom } from "@mantine/hooks";
import IconGitHubLogo from "~icons/tabler/brand-github";
import IconGraph from "~icons/tabler/graph";
import IconRSS from "~icons/tabler/rss";
import { YasunoriSpotlight } from "./components/yasunori-spotlight";
import { useIsMobile } from "./hooks/use-is-mobile";
Expand All @@ -38,13 +42,15 @@ export const links: LinksFunction = () => [
},
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Yellowtail&display=swap",
href: "https://fonts.googleapis.com/css2?family=Teko:[email protected]&family=Tiny5&display=swap",
},
];

export function Layout({ children }: { children: React.ReactNode }) {
const data = useRouteLoaderData<IndexLoader>("routes/_index");
const isIndexView = !!data;
const matches = useMatches();
const isEntry = !!matches.find(({ id }) => id === "routes/entries.$id");
const isStats = !!matches.find(({ id }) => id === "routes/stats");
const isMobile = useIsMobile();
const pinned = useHeadroom({ fixedAt: 120 });
const [opened, { toggle, close }] = useDisclosure();
Expand All @@ -65,11 +71,9 @@ export function Layout({ children }: { children: React.ReactNode }) {
<body>
<MantineProvider forceColorScheme="dark">
<AppShell
header={
isIndexView ? { height: 60, collapsed: !pinned } : undefined
}
header={!isEntry ? { height: 60, collapsed: !pinned } : undefined}
navbar={
isIndexView
!isEntry
? {
width: 300,
breakpoint: "sm",
Expand All @@ -79,27 +83,36 @@ export function Layout({ children }: { children: React.ReactNode }) {
}
padding="md"
>
{isIndexView && (
{!isEntry && (
<>
<AppShell.Header p="md">
<Group align="center" justify="space-between">
<Group gap="sm">
<Group gap="xs">
<Burger
opened={opened}
onClick={toggle}
hiddenFrom="sm"
size="sm"
/>
<Title
<Button
variant="transparent"
color="gray"
component={Title}
order={1}
size="h2"
style={{ fontFamily: "'Yellowtail', cursive" }}
size="compact-sm"
onClick={() => navigate("/")}
style={{
fontFamily: "'Tiny5', sans-serif",
fontSize: isMobile ? em(20) : em(30),
fontWeight: 400,
}}
>
Awesome Yasunori
</Title>
</Button>
</Group>
<Group>
<Group gap="sm">
<YasunoriSpotlight />
<IconGraph onClick={() => navigate("/stats")} />
<ActionIcon
component="a"
aria-label="rss feed"
Expand All @@ -125,33 +138,50 @@ export function Layout({ children }: { children: React.ReactNode }) {
</AppShell.Header>
<AppShell.Navbar p="md">
<AppShell.Section grow component={ScrollArea}>
{data?.map((d) => (
{isStats ? (
<NavLink
key={d.id}
onClick={() => {
navigate(`#${d.id}`, { replace: true });
navigate("#monthly-posts", { replace: true });
// モバイルのときは移動後にサイドバーを閉じる
if (isMobile) {
close();
}
}}
label={
<Group gap="xs">
<Text>{`${d.title}`}</Text>
<Text size="xs" c="dimmed">
{`#${d.id}`}
</Text>
<Text>Monthly Posts</Text>
</Group>
}
/>
))}
) : (
data?.map((d) => (
<NavLink
key={d.id}
onClick={() => {
navigate(`#${d.id}`, { replace: true });
// モバイルのときは移動後にサイドバーを閉じる
if (isMobile) {
close();
}
}}
label={
<Group gap="xs">
<Text>{`${d.title}`}</Text>
<Text size="xs" c="dimmed">
{`#${d.id}`}
</Text>
</Group>
}
/>
))
)}
</AppShell.Section>
</AppShell.Navbar>
</>
)}
<AppShell.Main
pt={
isIndexView
!isEntry
? `calc(${rem(60)} + var(--mantine-spacing-md))`
: undefined
}
Expand Down
47 changes: 47 additions & 0 deletions packages/web/app/routes/stats/aggregate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { SerializeFrom } from "@remix-run/cloudflare";
import type { IndexLoader } from "../../routes/_index/loader";

interface MonthlyPostsAggregatedData {
year: number;
month: number;
yearMonth: string;
amount: number;
}

export function monthlyPostsAggregate(
source: SerializeFrom<IndexLoader>,
): MonthlyPostsAggregatedData[] {
// 年月ごとの投稿数をカウントする
const counts = new Map<string, number>();
for (const data of source) {
const dateParts = data.date.split("-");
const yearMonth = `${dateParts.at(0)}-${dateParts.at(1)}`;
if (!counts.has(yearMonth)) {
counts.set(yearMonth, 1);
} else {
counts.set(yearMonth, (counts.get(yearMonth) ?? 0) + 1);
}
}

// 集計配列を作成する
const aggregated: MonthlyPostsAggregatedData[] = [];
for (const [yearMonth, count] of counts.entries()) {
const dateParts = yearMonth.split("-");
const year = Number(dateParts.at(0));
const month = Number(dateParts.at(1));
aggregated.push({
year,
month,
yearMonth,
amount: count,
});
}

// 年月の昇順でソートする
aggregated.sort((a, b) => {
if (a.year !== b.year) return a.year - b.year;
return a.month - b.month;
});

return aggregated;
}
28 changes: 28 additions & 0 deletions packages/web/app/routes/stats/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { LineChart } from "@mantine/charts";
import { Stack, Text, Title } from "@mantine/core";
import { useLoaderData } from "@remix-run/react";
import type { IndexLoader } from "../_index/loader";
export { indexLoader as loader } from "../_index/loader";
import "@mantine/charts/styles.css";
import { monthlyPostsAggregate } from "./aggregate";

export default function Stats() {
const source = useLoaderData<IndexLoader>();
const monthlyPostsAggregateData = monthlyPostsAggregate(source);
return (
<Stack gap="xl">
<Stack id="monthly-posts" gap="lg">
<Title order={2} size="h2">
Monthly Posts
</Title>
<LineChart
h={300}
data={monthlyPostsAggregateData}
dataKey="yearMonth"
series={[{ name: "amount", label: "Posts" }]}
/>
</Stack>
<Text>More statistics are being implemented.</Text>
</Stack>
);
}
2 changes: 2 additions & 0 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
},
"dependencies": {
"@awesome-yasunori/api": "workspace:*",
"@mantine/charts": "^7.13.2",
"@mantine/core": "^7.13.1",
"@mantine/hooks": "^7.13.1",
"@mantine/spotlight": "^7.13.2",
Expand All @@ -27,6 +28,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^9.0.1",
"recharts": "2",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.0"
},
Expand Down
Loading

0 comments on commit 9623c5d

Please sign in to comment.