Skip to content

Commit 2eb039f

Browse files
committed
Squashed commit of the following:
commit 236df93 Author: Yuito Akatsuki (Tani Yutaka) <yuito@yuito-it.jp> Date: Wed Nov 19 16:10:49 2025 +0900 Update src/server/api/items.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> commit efcc0a1 Author: Yuito Akatsuki (Tani Yutaka) <yuito@yuito-it.jp> Date: Wed Nov 19 16:10:31 2025 +0900 Update src/client/components/SidebarArticles.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> commit 2612b40 Author: Yuito Akatsuki (Tani Yutaka) <yuito@yuito-it.jp> Date: Wed Nov 19 16:09:41 2025 +0900 Update src/client/components/SidebarArticles.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> commit d53e280 Author: Yuito Akatsuki (Tani Yutaka) <yuito@yuito-it.jp> Date: Wed Nov 19 16:09:18 2025 +0900 Update src/lib/file-system-repo.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> commit c8e8a16 Author: Yuito Akatsuki (Tani Yutaka) <yuito@yuito-it.jp> Date: Wed Nov 19 14:57:09 2025 +0900 スタイルを調整し同階層を表す縦線を追加 commit 69c4050 Author: Yuito Akatsuki (Tani Yutaka) <yuito@yuito-it.jp> Date: Wed Nov 19 14:45:41 2025 +0900 ディレクトリを一番最初に出すようにする commit dda0e55 Author: Yuito Akatsuki (Tani Yutaka) <yuito@yuito-it.jp> Date: Wed Nov 19 14:41:59 2025 +0900 再起的に階層構造として表示する commit c841754 Author: Yuito Akatsuki (Tani Yutaka) <yuito@yuito-it.jp> Date: Wed Nov 19 14:14:53 2025 +0900 ディレクトリ階層を表示するようにする commit 1aa8894 Author: Yuito Akatsuki (Tani Yutaka) <yuito@yuito-it.jp> Date: Wed Nov 19 12:28:03 2025 +0900 ディレクトリも含めるように変更 commit 55f88c7 Author: Yuito Akatsuki (Tani Yutaka) <yuito@yuito-it.jp> Date: Wed Nov 19 12:13:07 2025 +0900 recursiveにファイル名を取得するように変更する commit d8813e2 Author: Yuito Akatsuki (Tani Yutaka) <yuito@yuito-it.jp> Date: Wed Nov 19 12:01:36 2025 +0900 開発用にwatchからnode_modulesとかをignoreしておく commit 5a33c35 Author: Yuito Akatsuki (Tani Yutaka) <yuito@yuito-it.jp> Date: Wed Nov 19 11:59:39 2025 +0900 update dependics
1 parent 2230739 commit 2eb039f

File tree

7 files changed

+180
-47
lines changed

7 files changed

+180
-47
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"@types/jest": "^30.0.0",
2929
"@types/node": "^22.15.30",
3030
"@types/react": "^19.1.13",
31-
"@types/react-dom": "^19.1.6",
31+
"@types/react-dom": "^19.1.13",
3232
"@types/webpack": "^5.28.5",
3333
"@types/ws": "^8.18.1",
3434
"@typescript-eslint/eslint-plugin": "^7.0.0",
@@ -43,8 +43,8 @@
4343
"lint-staged": "^15.5.1",
4444
"npm-run-all": "^4.1.5",
4545
"prettier": "^3.6.2",
46-
"react": "^19.1.1",
47-
"react-dom": "^19.1.0",
46+
"react": "^19.1.13",
47+
"react-dom": "^19.1.13",
4848
"react-router": "^7.8.2",
4949
"ts-jest": "^29.4.4",
5050
"ts-loader": "^9.5.4",

src/client/components/SidebarArticles.tsx

Lines changed: 139 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -59,26 +59,111 @@ export const SidebarArticles = ({ items, sortType, articleState }: Props) => {
5959
localStorage.setItem(StorageName[articleState], isDetailsOpen.toString());
6060
}, [isDetailsOpen]);
6161

62+
// build recursive tree from item.parent (segments array)
63+
const topLevelItems: ItemViewModel[] = [];
64+
65+
type TreeNode = {
66+
name: string;
67+
items: ItemViewModel[];
68+
children: { [name: string]: TreeNode };
69+
};
70+
71+
const roots: { [name: string]: TreeNode } = {};
72+
73+
const addToTree = (segments: string[], item: ItemViewModel) => {
74+
const rootName = segments[0];
75+
if (!roots[rootName])
76+
roots[rootName] = { name: rootName, items: [], children: {} };
77+
let node = roots[rootName];
78+
const rest = segments.slice(1);
79+
if (rest.length === 0) {
80+
node.items.push(item);
81+
return;
82+
}
83+
for (const seg of rest) {
84+
if (!node.children[seg])
85+
node.children[seg] = { name: seg, items: [], children: {} };
86+
node = node.children[seg];
87+
}
88+
node.items.push(item);
89+
};
90+
91+
items.forEach((item) => {
92+
if (!item.parent || item.parent.length === 0) {
93+
topLevelItems.push(item);
94+
} else {
95+
addToTree(item.parent, item);
96+
}
97+
});
98+
99+
const countSubtreeItems = (node: TreeNode): number =>
100+
node.items.length +
101+
Object.values(node.children).reduce((s, c) => s + countSubtreeItems(c), 0);
102+
103+
const renderNode = (node: TreeNode, path: string) => {
104+
const cmp = compare[sortType];
105+
return (
106+
<li key={path}>
107+
<details css={articleDetailsStyle} open>
108+
<summary css={articleSummaryStyle}>
109+
{node.name}
110+
<span css={articleSectionTitleCountStyle}>
111+
{countSubtreeItems(node)}
112+
</span>
113+
</summary>
114+
<ul>
115+
{Object.values(node.children)
116+
.sort((a, b) => a.name.localeCompare(b.name))
117+
.map((child) => renderNode(child, `${path}/${child.name}`))}
118+
119+
{[...node.items].sort(cmp).map((item) => (
120+
<li key={item.items_show_path}>
121+
<Link css={articlesListItemStyle} to={item.items_show_path}>
122+
<MaterialSymbol
123+
fill={item.modified && articleState !== "Draft"}
124+
>
125+
note
126+
</MaterialSymbol>
127+
<span css={articleListItemInnerStyle}>
128+
{item.modified && articleState !== "Draft" && "(差分あり) "}
129+
{item.title}
130+
</span>
131+
</Link>
132+
</li>
133+
))}
134+
</ul>
135+
</details>
136+
</li>
137+
);
138+
};
139+
62140
return (
63141
<details css={articleDetailsStyle} open={isDetailsOpen}>
64142
<summary css={articleSummaryStyle} onClick={toggleAccordion}>
65143
{ArticleState[articleState]}
66144
<span css={articleSectionTitleCountStyle}>{items.length}</span>
67145
</summary>
68146
<ul>
69-
{items.sort(compare[sortType]).map((item) => (
70-
<li key={item.items_show_path}>
71-
<Link css={articlesListItemStyle} to={item.items_show_path}>
72-
<MaterialSymbol fill={item.modified && articleState !== "Draft"}>
73-
note
74-
</MaterialSymbol>
75-
<span css={articleListItemInnerStyle}>
76-
{item.modified && articleState !== "Draft" && "(差分あり) "}
77-
{item.title}
78-
</span>
79-
</Link>
80-
</li>
81-
))}
147+
{Object.values(roots)
148+
.sort((a, b) => a.name.localeCompare(b.name))
149+
.map((r) => renderNode(r, r.name))}
150+
151+
{topLevelItems.length > 0 &&
152+
[...topLevelItems].sort(compare[sortType]).map((item) => (
153+
<li key={item.items_show_path}>
154+
<Link css={articlesListItemStyle} to={item.items_show_path}>
155+
<MaterialSymbol
156+
fill={item.modified && articleState !== "Draft"}
157+
>
158+
note
159+
</MaterialSymbol>
160+
<span css={articleListItemInnerStyle}>
161+
{item.modified && articleState !== "Draft" && "(差分あり) "}
162+
{item.title}
163+
</span>
164+
</Link>
165+
</li>
166+
))}
82167
</ul>
83168
</details>
84169
);
@@ -93,6 +178,44 @@ const articleDetailsStyle = css({
93178
"&[open] > summary::before": {
94179
content: "'expand_more'",
95180
},
181+
// nested lists: draw vertical guide lines inside the padded area
182+
"& ul": {
183+
listStyle: "none",
184+
margin: 0,
185+
paddingLeft: getSpace(1),
186+
},
187+
"& ul ul": {
188+
position: "relative",
189+
paddingLeft: getSpace(3),
190+
},
191+
"& ul ul::before": {
192+
content: "''",
193+
position: "absolute",
194+
left: getSpace(3),
195+
top: 0,
196+
bottom: 0,
197+
width: 1,
198+
backgroundColor: Colors.gray20,
199+
},
200+
"& ul ul > li": {
201+
paddingLeft: getSpace(1.5),
202+
},
203+
"& ul ul ul": {
204+
position: "relative",
205+
paddingLeft: getSpace(4),
206+
},
207+
"& ul ul ul::before": {
208+
content: "''",
209+
position: "absolute",
210+
left: getSpace(3),
211+
top: 0,
212+
bottom: 0,
213+
width: 1,
214+
backgroundColor: Colors.gray20,
215+
},
216+
"& ul ul ul > li": {
217+
paddingLeft: getSpace(1.5),
218+
},
96219
});
97220

98221
const articleSummaryStyle = css({
@@ -137,9 +260,9 @@ const articlesListItemStyle = css({
137260
fontSize: Typography.body2,
138261
gap: getSpace(1),
139262
lineHeight: LineHeight.bodyDense,
140-
padding: `${getSpace(3 / 4)}px ${getSpace(5 / 2)}px ${getSpace(
141-
3 / 4,
142-
)}px ${getSpace(3 / 2)}px`,
263+
padding: `${getSpace(3 / 4)}px ${getSpace(5 / 2)}px ${getSpace(3 / 4)}px ${getSpace(
264+
3,
265+
)}px`,
143266
whiteSpace: "nowrap",
144267
textOverflow: "ellipsis",
145268

src/lib/file-system-repo.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -206,17 +206,22 @@ export class FileSystemRepo {
206206
}
207207

208208
private parseFilename(filename: string) {
209-
return path.basename(filename, ".md");
209+
return filename.replace(/\.md$/, "");
210210
}
211211

212212
private getFilePath(uuid: string, remote: boolean = false) {
213213
return path.join(this.getRootOrRemotePath(remote), this.getFilename(uuid));
214214
}
215215

216216
private async getItemFilenames(remote: boolean = false) {
217-
return await fs.readdir(
218-
this.getRootOrRemotePath(remote),
219-
FileSystemRepo.fileSystemOptions(),
217+
return (
218+
await fs.readdir(
219+
this.getRootOrRemotePath(remote),
220+
FileSystemRepo.fileSystemOptions(),
221+
)
222+
).filter(
223+
(itemFilename) =>
224+
/\.md$/.test(itemFilename) && !itemFilename.startsWith(".remote/"),
220225
);
221226
}
222227

@@ -246,6 +251,8 @@ export class FileSystemRepo {
246251
private static fileSystemOptions() {
247252
return {
248253
encoding: "utf8",
254+
withFileTypes: false,
255+
recursive: true,
249256
} as const;
250257
}
251258

@@ -325,12 +332,10 @@ export class FileSystemRepo {
325332
async loadItems(): Promise<QiitaItem[]> {
326333
const itemFilenames = await this.getItemFilenames();
327334

328-
const promises = itemFilenames
329-
.filter((itemFilename) => /\.md$/.test(itemFilename))
330-
.map(async (itemFilename) => {
331-
const basename = this.parseFilename(itemFilename);
332-
return await this.loadItemByBasename(basename);
333-
});
335+
const promises = itemFilenames.map(async (itemFilename) => {
336+
const basename = this.parseFilename(itemFilename);
337+
return await this.loadItemByBasename(basename);
338+
});
334339

335340
const items = excludeNull(await Promise.all(promises));
336341
return items;

src/lib/view-models/items.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export type ItemViewModel = {
55
title: string;
66
updated_at: string;
77
modified: boolean;
8+
parent: string[];
89
};
910

1011
export type ItemsIndexViewModel = {

src/server/api/items.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const itemsIndex = async (req: Express.Request, res: Express.Response) => {
2727
title: item.title,
2828
updated_at: item.updatedAt,
2929
modified: item.modified,
30+
parent: item.name.split("/").slice(0, -1),
3031
};
3132

3233
if (item.id) {

src/server/app.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@ export function startLocalChangeWatcher({
6565
watchPath: string;
6666
}) {
6767
const wsServer = new WebSocketServer({ server });
68-
const watcher = chokidar.watch(watchPath);
68+
const watcher = chokidar.watch(watchPath, {
69+
ignored: /node_modules|\.git/,
70+
persistent: true,
71+
});
6972
watcher.on("change", () => {
7073
wsServer.clients.forEach((client) => {
7174
if (client.readyState === WebSocket.OPEN) {

yarn.lock

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,10 +1315,10 @@
13151315
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
13161316
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
13171317

1318-
"@types/react-dom@^19.1.6":
1319-
version "19.1.6"
1320-
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.6.tgz#4af629da0e9f9c0f506fc4d1caa610399c595d64"
1321-
integrity sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==
1318+
"@types/react-dom@^19.1.13":
1319+
version "19.2.3"
1320+
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.2.3.tgz#c1e305d15a52a3e508d54dca770d202cb63abf2c"
1321+
integrity sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==
13221322

13231323
"@types/react@^19.1.13":
13241324
version "19.1.13"
@@ -5919,12 +5919,12 @@ raw-body@^3.0.0:
59195919
iconv-lite "0.6.3"
59205920
unpipe "1.0.0"
59215921

5922-
react-dom@^19.1.0:
5923-
version "19.1.0"
5924-
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.1.0.tgz#133558deca37fa1d682708df8904b25186793623"
5925-
integrity sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==
5922+
react-dom@^19.1.13:
5923+
version "19.2.0"
5924+
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.0.tgz#00ed1e959c365e9a9d48f8918377465466ec3af8"
5925+
integrity sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==
59265926
dependencies:
5927-
scheduler "^0.26.0"
5927+
scheduler "^0.27.0"
59285928

59295929
react-is@^16.13.1, react-is@^16.7.0:
59305930
version "16.13.1"
@@ -5944,10 +5944,10 @@ react-router@^7.8.2:
59445944
cookie "^1.0.1"
59455945
set-cookie-parser "^2.6.0"
59465946

5947-
react@^19.1.1:
5948-
version "19.1.1"
5949-
resolved "https://registry.yarnpkg.com/react/-/react-19.1.1.tgz#06d9149ec5e083a67f9a1e39ce97b06a03b644af"
5950-
integrity sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==
5947+
react@^19.1.13:
5948+
version "19.2.0"
5949+
resolved "https://registry.yarnpkg.com/react/-/react-19.2.0.tgz#d33dd1721698f4376ae57a54098cb47fc75d93a5"
5950+
integrity sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==
59515951

59525952
read-pkg@^3.0.0:
59535953
version "3.0.0"
@@ -6214,10 +6214,10 @@ safe-regex-test@^1.1.0:
62146214
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
62156215
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
62166216

6217-
scheduler@^0.26.0:
6218-
version "0.26.0"
6219-
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.26.0.tgz#4ce8a8c2a2095f13ea11bf9a445be50c555d6337"
6220-
integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==
6217+
scheduler@^0.27.0:
6218+
version "0.27.0"
6219+
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd"
6220+
integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==
62216221

62226222
schema-utils@^4.3.0, schema-utils@^4.3.2:
62236223
version "4.3.2"

0 commit comments

Comments
 (0)