Zennで自分のスクラップ記録を集計する拡張機能を作りたいかもしれない
スクラップにあらゆる勉強を寄せているので、スクラップを見れば毎月の振り返りができる。
- 今月Closeされたスクラップは何本?
- スクラップ1本あたりの平均コメント数は?
程度の集計を拡張機能からサクッとできたら嬉しそう
https://zenn.dev/api/scraps?username={username}
で、ユーザーごとにすべてのスクラップを取ってこれるっぽい
JSONから型を生成させたらこんな感じ
export interface Scrap {
scraps?: (ScrapsEntity)[] | null;
next_page?: null;
}
export interface ScrapsEntity {
id: number;
post_type: string;
user_id: number;
slug: string;
title: string;
closed: boolean;
closed_at?: string | null;
archived: boolean;
liked_count: number;
can_others_post: boolean;
comments_count: number;
created_at: string;
last_comment_created_at: string;
should_noindex: boolean;
path: string;
topics?: (TopicsEntity)[] | null;
user: User;
}
export interface TopicsEntity {
id: number;
name: string;
display_name: string;
taggings_count: number;
image_url?: string | null;
}
export interface User {
id: number;
username: string;
name: string;
avatar_small_url: string;
}
100件くらい超えたらページネーションあるんかもしれない。どうだろ
イメージ
- archiveされてないものを全体とする
- デフォルトで総本数、総コメント数、平均コメント数を計測
- カレンダー的なUIで対象日にfilterをかけられる
- それとは別によく使いそうなfilterをボタンで用意
- 直近1, 2, 3, 12ヶ月
- 今年
- 今月
カレンダーはあとでいいや
React*TSで拡張機能作りたい
一旦開発者ツールのコンソールでやってみるか
雑にこんな感じ
const data = await fetch('https://zenn.dev/api/scraps?username=ikenohi').then(res => res.json())
const allActiveScrap = data.scraps.filter(item => !item.archived)
const sum = (a,b) => a+b
const totalScraps = allActiveScrap.length
const totalComments = allActiveScrap.map(item => item.comments_count).reduce(sum,0)
const message = `総スクラップ数:${totalScraps}, 総コメント数:${totalComments}`
→総スクラップ数:21, 総コメント数:710
const avarageComments = totalComments / totalScraps
33.80952380952381
十分だな
これのあとに
const data = await fetch('https://zenn.dev/api/scraps?username=ikenohi').then(res => res.json())
const allActiveScrap = data.scraps.filter(item => !item.archived)
const sum = (a,b) => a+b
const totalScraps = allActiveScrap.length
const totalComments = allActiveScrap.map(item => item.comments_count).reduce(sum,0)
const avarageComments = Math.round(totalComments / totalScraps)
const msg = `総スクラップ数:${totalScraps} \n総コメント数:${totalComments} \n平均コメント数:${avarageComments}`
これをすればやりたいことは大体できる
copy(msg)
あとは日付のフィルター
そういえばJSの日付のフォーマット雰囲気で使ってるからあとで別のスクラップ立てて調べよう
月末と月初の取得はこんな感じ?なんもわからん
const startOfMonth = (month) => {
const date = new Date()
if (month) date.setMonth(month - 1)
date.setDate(1)
date.setHours(0)
date.setMinutes(0)
date.setSeconds(0)
return date
}
const endOfMonth = (month) => {
const date = new Date()
if (month) {
date.setMonth(month)
} else {
date.setMonth(date.getMonth() + 1)
}
date.setDate(0)
date.setHours(23)
date.setMinutes(59)
date.setSeconds(59)
return date
}
startいらんか。endで挟めばいいもんな
年も指定したいか
const endOfMonth = (month, year) => {
const date = new Date()
if (year) date.setYear(year)
if (month) {
date.setMonth(month)
} else {
date.setMonth(date.getMonth() + 1)
}
date.setDate(0)
date.setHours(23)
date.setMinutes(59)
date.setSeconds(59)
return date
}
いや、-2とかで書けるらしいからこうしよう
const endOfMonth = (month) => {
const date = new Date()
if (month) {
date.setMonth(month + 1)
} else {
date.setMonth(date.getMonth() + 1)
}
date.setDate(0)
date.setHours(23)
date.setMinutes(59)
date.setSeconds(59)
return date
}
こうすればこうかけるはず
- n月中:e(-1) < target < e()
- 直近nヶ月:e(-n) < target < now
直近nヶ月はeつかっちゃだめか
ふつうにnow.setMonth(-n)と比べればいいんだ
const isThisMonth = (target) => {
return target && endOfMonth(-1) < target && target < endOfMonth()
}
const nMonthBefore = (month) => {
const date = new Date()
date.setMonth(-month)
date.setHours(0)
date.setMinutes(0)
date.setSeconds(0)
return date
}
const isLastNMonth = (target, n) => {
const l = n === undefined ? 1 : n
const now = new Date()
const before = nMonthBefore(l)
return target && before < target && target < now
}
const 今月中 = allActiveScrap.filter(item => isThisMonth(new Date(item.closed_at)))
const 直近3ヶ月 = allActiveScrap.filter(item => isLastNMonth(new Date(item.closed_at), 3))
こんな感じか
そろそろエディタで書くかー
これを使わせていただきます
Step
Clone this repository.
Change name and description in package.json => Auto synchronize with manifest
Run yarn install or npm i (check your node version >= 16.6, recommended >= 18)
Run yarn dev or npm run dev
Load Extension on Chrome
Open - Chrome browser
Access - chrome://extensions
Check - Developer mode
Find - Load unpacked extension
Select - dist folder in this project (after dev or build)
If you want to build in production, Just run yarn build or npm run build.
usernameはlocalstrageのzenn_current_userに入っている。
const currentUserJson = localStorage.getItem('zenn_current_user')
const s = JSON.parse(currentUserJson)
const url = `https://zenn.dev/api/scraps?username=${s.cachedUser.username}`
自分が欲しい物、拡張機能じゃない気がしてきた
改めてユースケースを確認
「毎月末や毎四半期、毎年末などに、スクラップの総数やコメント数、内容を振り返ることでその期間を効果的に振り返る。」
欲しい機能
- 毎月末や毎四半期、毎年末ごとにこの情報を算出してクリップボードにコピー
- その期間中にCloseされたスクラップの総数、平均コメント数
- その期間中にCloseされたスクラップのタイトルとリンクを
[title](link)
のかたちでリスト化したもの
これだけでいいわ
冷静になって整理する。
// 素材となるデータの取得方法
const data = await fetch('https://zenn.dev/api/scraps?username=ikenohi').then(res => res.json())
const allActiveScrap = data.scraps.filter(item => !item.archived)
// Scrapを入れれば集計してくれる
const countScraps = (scraps) => {
const sum = (a,b) => a+b
const totalScrapsCount = scraps.length
const totalCommentsCount = scraps.map(item => item.comments_count).reduce(sum,0)
const avarageCommentsCount = Math.round(totalCommentsCount / totalScrapsCount)
const result = `総スクラップ数:${totalScrapsCount} \n総コメント数:${totalCommentsCount} \n平均コメント数:${avarageCommentsCount}`
return result
}
/* Scrapを期間指定でfilterするための関数群 */
// 月指定で月末23:59:59のDateを返してくれる
const endOfMonth = (month) => {
const date = new Date()
if (month) {
date.setMonth(month + 1)
} else {
date.setMonth(date.getMonth() + 1)
}
date.setDate(0)
date.setHours(23)
date.setMinutes(59)
date.setSeconds(59)
return date
}
// 今月中ならtrueを返す
const isThisMonth = (target) => {
return target && endOfMonth(-1) < target && target < endOfMonth()
}
// 今日からnヶ月前の日の0:00:00のDateを返してくれる
const nMonthBefore = (month) => {
const date = new Date()
date.setMonth(-month)
date.setHours(0)
date.setMinutes(0)
date.setSeconds(0)
return date
}
// 今日からnMonthBeforeの間ならtrueを返す
const isLastNMonth = (target, n) => {
const l = n === undefined ? 1 : n
const now = new Date()
const before = nMonthBefore(l)
return target && before < target && target < now
}
こんな感じで使える。
const 今月中 = allActiveScrap.filter(item => isThisMonth(new Date(item.closed_at)))
const 直近3ヶ月 = allActiveScrap.filter(item => isLastNMonth(new Date(item.closed_at), 3))
これでいいんか?2/1に1月の振り返りをしたくなったらどうする?
ゴールから描こう
月の振り返り(1) // 今年1月の振り返り結果をクリップボードにコピー
四半期の振り返り(1) // 今年の四半期Q1(つまり1~3月)の振り返り結果をクリップボードにコピー
いや、やっぱり直近3ヶ月みたいな振り返り方はしたい。3,4,5月の〜も欲しい
この期間の振り返り(1) // 今年1月の振り返り結果をクリップボードにコピー
この期間の振り返り(1,4) // 今年1月~4月の振り返り結果をクリップボードにコピー
これだと年またいだときどうすんねん
振り返り(a,b)
でa>=b
なら去年のa月から今年のb月にするか。
全体像?
const reflectionWithScrap = (n, m) => {
const result = !m
? getReflectionForMonth(n)
: getReflectionBetweenMonths(n, m);
navigator.clipboard.writeText(result);
};
明らかに抽象化できるけどとりあえず思いつきで書いてる
const getReflectionForMonth = (n) => {
// 今年のn-1月末よりあと、今年のn月末以前のスクラップを対象に集計
const filterFn = (target) => {
return target && endOfMonth(n - 1) < target && target < endOfMonth(n);
};
const scrapsToBeCovered = scraps.filter((item) => filterFn(item.closed_at));
const countSummary = countScraps(scrapsToBeCovered);
const list = listScraps(scrapsToBeCovered);
return `## サマリー \n${countSummary} \n## 対象スクラップ \n${list}`;
};
const getReflectionBetweenMonths = (n, m) => {
if (n >= m) {
// 昨年のn-1月末よりあと、今年のm月末以前のスクラップを対象に集計
} else {
// 今年のn-1月末よりあと、今年のm月末以前のスクラップを対象に集計
}
return;
};
やべ、全然頭働かない
const generateSummary = (scraps) => {
const countSummary = countScraps(scraps);
const list = listScraps(scraps);
return `## サマリー \n${countSummary} \n## 対象スクラップ \n${list}`;
};
// スクラップのリンク付き箇条書きリストを作成
const listScraps = (scraps) => {
const bulletedList = scraps.map(
(scrap) => `- [${scrap.title}](https://zenn.dev/${scrap.path})`
);
return bulletedList.join("\n");
};
結果をそのまま貼り付け
サマリー
総スクラップ数:22
総コメント数:760
平均コメント数:35
対象スクラップ
- 私は雰囲気でブラウザを使っています。
- MSWの門前
- scaffdogとHygenの比較
- 【読書メモ】プラトン『饗宴』(光文社)
- 直近で作りたいスクラップのアイデアを吐き出しておく
- Zennで自分のスクラップ記録を集計する拡張機能を作りたいかもしれない
- npm install uhyo
- TypeScriptの型 入門/初級/演習
- テスト基盤としてのStorybook7
- 【読書メモ】ダニエル・E・リーバーマン『運動の神話』上
- Storybook7探訪
- 2023年に読めたらいいなと思う本をリストアップ
- はじめまして、NextAuthさん、Rainbowkitさん
- HIGのつまみ食い
- Next13を落ち着いて知る
- 踊ろよ、fish
- npm install humor
- Next13 × Storybook7を試す
- CS50入学説明会
- 「フロントエンドの基礎知識がある」って何を知ってたら言えるの?
- 自分のNFTを作ってみる
- fzf探訪
いいね!あとはフィルターだけか
あー、サマリーの出力自体は全部一緒だからfilterだけ場合分けすればよかったんか
endOfYearで年を指定できればこうかけるはず
const reflectionWithScrap = (n, m) => {
const filterFn = (target) => {
const args = {
monthly: {
first: [n - 1],
second: [n],
},
between: {
first: [n - 1],
second: [m],
},
acrossYear: {
first: [n - 1, thisYear - 1],
second: [m],
},
};
const key = !m ? "monthly" : n >= m ? "acrossYear" : "between";
return (
target &&
endOfMonth(...args[key].first) < target &&
target < endOfMonth(...args[key].second)
);
};
const scrapsToBeCovered = scraps.filter((item) => filterFn(item.closed_at));
const summary = generateSummary(scrapsToBeCovered);
navigator.clipboard.writeText(summary);
};
見通しを良くする
const reflectionWithScrap = (n, m) => {
const filterFn = defineFilterFn(n, m);
const scrapsToBeCovered = scraps.filter((item) => filterFn(item.closed_at));
const summary = generateSummary(scrapsToBeCovered);
navigator.clipboard.writeText(summary);
};
const thisYear = new Date().getFullYear();
const defineFilterArgs = (n, m) => {
const args = {
monthly: {
first: [n - 1],
second: [n],
},
between: {
first: [n - 1],
second: [m],
},
acrossYear: {
first: [n - 1, thisYear - 1],
second: [m],
},
};
const key = !m ? "monthly" : n >= m ? "acrossYear" : "between";
return args[key];
};
const defineFilterFn = (n, m) => (target) => {
const filterArgs = defineFilterArgs(n, m);
return (
target &&
endOfMonth(...filterArgs.first) < target &&
target < endOfMonth(...filterArgs.second)
);
};
長くなったけどたぶんこういうこと
/*
* === データの取得の部 ===
*/
const data = await fetch("https://zenn.dev/api/scraps?username=ikenohi").then(
(res) => res.json()
);
const allActiveScrap = data.scraps.filter((item) => !item.archived);
/*
* === 処理の総合の部 ===
*/
export const reflectionWithScrap = (n, m) => {
const filterFn = defineFilterFn(n, m);
const scrapsToBeCovered = allActiveScrap.filter((item) =>
filterFn(item.closed_at)
);
const summary = generateSummary(scrapsToBeCovered);
navigator.clipboard.writeText(summary);
};
/*
* === フィルターの部 ===
*/
const thisYear = new Date().getFullYear();
// n, mの値によってfilterの引数を変える
const defineFilterArgs = (n, m) => {
const args = {
monthly: {
first: [n - 1],
second: [n],
},
between: {
first: [n - 1],
second: [m],
},
acrossYear: {
first: [n - 1, thisYear - 1],
second: [m],
},
};
const key = !m ? "monthly" : n >= m ? "acrossYear" : "between";
return args[key];
};
// 切り出すためにカリー化しただけ
const defineFilterFn = (n, m) => (target) => {
const filterArgs = defineFilterArgs(n, m);
return (
target &&
endOfMonth(...filterArgs.first) < target &&
target < endOfMonth(...filterArgs.second)
);
};
// その月の末日23:59:59のDateを返してくれる
const endOfMonth = (month, year) => {
const date = new Date();
if (year) date.setYear(year);
if (month) {
date.setMonth(month);
} else {
date.setMonth(date.getMonth() + 1);
}
date.setDate(0);
date.setHours(23);
date.setMinutes(59);
date.setSeconds(59);
return date;
};
/*
* === サマリー生成の部 ===
*/
// 本体
const generateSummary = (scraps) => {
const countSummary = countScraps(scraps);
const list = listScraps(scraps);
return `## サマリー \n${countSummary} \n## 対象スクラップ \n${list}`;
};
// 集計
const countScraps = (scraps) => {
const sum = (a, b) => a + b;
const totalScrapsCount = scraps.length;
const totalCommentsCount = scraps
.map((item) => item.comments_count)
.reduce(sum, 0);
const avarageCommentsCount = Math.round(
totalCommentsCount / totalScrapsCount
);
const result = `総スクラップ数:${totalScrapsCount} \n総コメント数:${totalCommentsCount} \n平均コメント数:${avarageCommentsCount}`;
return result;
};
// リンク月箇条書きリストの作成
const listScraps = (scraps) => {
const bulletedList = scraps.map(
(scrap) => `- [${scrap.title}](https://zenn.dev/${scrap.path})`
);
return bulletedList.join("\n");
};
ていうかふつうにmonth/year指定させれば変な分岐いらんかったんでは
reflectionWithScrap({year: 2023, month:1},{year:2023, month:1}) // 2023年1月の振り返り
reflectionWithScrap({year: 2023, month:1},{year:2023, month:3}) // 2023年1~3月の振り返り
reflectionWithScrap({year: 2022, month:12},{year:2023, month:3}) // 2022年12~2023年3月の振り返り
これの特例として今月の振り返りすればよかったんでは
const reflectionOfThisMonth = () => {
const year = new Date().getFullYear()
const month = new Date().getMonth() + 1
return reflectionWithScrap({year, month},{year, month})
}
みたいな
美いの基準によるか
いやでもそっちのがいいわ
/*
* === データの取得の部 ===
*/
const data = await fetch("https://zenn.dev/api/scraps?username=ikenohi").then(
(res) => res.json()
);
const allActiveScrap = data.scraps.filter((item) => !item.archived);
/*
* === 処理の総合の部 ===
*/
const reflectionWithScrap = (from, to) => {
const filterFn = (target) => {
return (
target &&
endOfMonth(from.month, from.year) < target &&
target < endOfMonth(to.month, to.year)
);
};
const scrapsToBeCovered = allActiveScrap.filter((item) =>
filterFn(item.closed_at)
);
const summary = generateSummary(scrapsToBeCovered);
// navigator.clipboard.writeText(summary);
return summary;
};
/*
* === フィルターの部 ===
*/
// その月の末日23:59:59のDateを返してくれる
const endOfMonth = (month, year) => {
const date = new Date();
if (year) date.setYear(year);
if (month) {
date.setMonth(month);
} else {
date.setMonth(date.getMonth() + 1);
}
date.setDate(0);
date.setHours(23);
date.setMinutes(59);
date.setSeconds(59);
return date;
};
/*
* === サマリー生成の部 ===
*/
// 本体
const generateSummary = (scraps) => {
const countSummary = countScraps(scraps);
const list = listScraps(scraps);
return `## サマリー \n${countSummary} \n## 対象スクラップ \n${list}`;
};
// 集計
const countScraps = (scraps) => {
const sum = (a, b) => a + b;
const totalScrapsCount = scraps.length;
const totalCommentsCount = scraps
.map((item) => item.comments_count)
.reduce(sum, 0);
const avarageCommentsCount = Math.round(
totalCommentsCount / totalScrapsCount
);
const result = `総スクラップ数:${totalScrapsCount} \n総コメント数:${totalCommentsCount} \n平均コメント数:${avarageCommentsCount}`;
return result;
};
// リンク月箇条書きリストの作成
const listScraps = (scraps) => {
const bulletedList = scraps.map(
(scrap) => `- [${scrap.title}](https://zenn.dev/${scrap.path})`
);
return bulletedList.join("\n");
};
あ、でも動かんw
あ、targetをDateで包んであげな
よし、2022/12の月末から2023/1の月末まで(つまり2023/1)のサマリを出力
サマリー
総スクラップ数:12
総コメント数:362
平均コメント数:30
対象スクラップ
いろいろあったけどたぶんこうです
const reflectionOfThisMonth = () => {
const thisYear = new Date().getFullYear();
const thisMonth = new Date().getMonth() + 1;
const from = {
year: thisMonth === 1 ? thisYear - 1 : thisYear,
month: thisMonth === 1 ? 12 : thisMonth - 1,
};
const to = {
year: thisYear,
month: thisMonth,
};
return reflectionWithScrap(from, to);
};
/*
* === データの取得の部 ===
*/
const data = await fetch("https://zenn.dev/api/scraps?username=ikenohi").then(
(res) => res.json()
);
const allActiveScrap = data.scraps.filter((item) => !item.archived);
/*
* === 処理の総合の部 ===
*/
const reflectionWithScrap = (from, to) => {
const filterFn = (target) => {
return (
target &&
endOfMonth(from.month, from.year) < target &&
target < endOfMonth(to.month, to.year)
);
};
const scrapsToBeCovered = allActiveScrap.filter((item) =>
filterFn(new Date(item.closed_at))
);
const summary = generateSummary(scrapsToBeCovered);
// navigator.clipboard.writeText(summary);
return summary;
};
/*
* === フィルターの部 ===
*/
// その月の末日23:59:59のDateを返してくれる
const endOfMonth = (month, year) => {
const date = new Date();
if (year) date.setYear(year);
if (month) {
date.setMonth(month);
} else {
date.setMonth(date.getMonth() + 1);
}
date.setDate(0);
date.setHours(23);
date.setMinutes(59);
date.setSeconds(59);
return date;
};
/*
* === サマリー生成の部 ===
*/
// 本体
const generateSummary = (scraps) => {
const countSummary = countScraps(scraps);
const list = listScraps(scraps);
return `## サマリー \n${countSummary} \n## 対象スクラップ \n${list}`;
};
// 集計
const countScraps = (scraps) => {
const sum = (a, b) => a + b;
const totalScrapsCount = scraps.length;
const totalCommentsCount = scraps
.map((item) => item.comments_count)
.reduce(sum, 0);
const avarageCommentsCount = Math.round(
totalCommentsCount / totalScrapsCount
);
const result = `総スクラップ数:${totalScrapsCount} \n総コメント数:${totalCommentsCount} \n平均コメント数:${avarageCommentsCount}`;
return result;
};
// リンク月箇条書きリストの作成
const listScraps = (scraps) => {
const bulletedList = scraps.map(
(scrap) => `- [${scrap.title}](https://zenn.dev/${scrap.path})`
);
return bulletedList.join("\n");
};
開発者ツールのときは、これのあとに
copy(reflectionOfThisMonth())
ほんとはTemperMonkeyなんかに登録してボタン一つでできるようになりたいけど今は疲れたのでスニペットで使うということにしとく
ふつうによくない迷い方してた、くやしい
このスクラップをクローズした直後の結果
サマリー
総スクラップ数:13
総コメント数:419
平均コメント数:32