-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #121 from times-yasunori/stats
Awesome Yasunori Stats
- Loading branch information
Showing
6 changed files
with
428 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"; | ||
|
@@ -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"; | ||
|
@@ -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(); | ||
|
@@ -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", | ||
|
@@ -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" | ||
|
@@ -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 | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.