Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"@types/jest": "^30.0.0",
"@types/node": "^22.15.30",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.6",
"@types/react-dom": "^19.1.13",
"@types/webpack": "^5.28.5",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^7.0.0",
Expand All @@ -43,8 +43,8 @@
"lint-staged": "^15.5.1",
"npm-run-all": "^4.1.5",
"prettier": "^3.6.2",
"react": "^19.1.1",
"react-dom": "^19.1.0",
"react": "^19.1.13",
"react-dom": "^19.1.13",
"react-router": "^7.8.2",
"ts-jest": "^29.4.4",
"ts-loader": "^9.5.4",
Expand Down
155 changes: 139 additions & 16 deletions src/client/components/SidebarArticles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,26 +59,111 @@ export const SidebarArticles = ({ items, sortType, articleState }: Props) => {
localStorage.setItem(StorageName[articleState], isDetailsOpen.toString());
}, [isDetailsOpen]);

// build recursive tree from item.parent (segments array)
const topLevelItems: ItemViewModel[] = [];

type TreeNode = {
name: string;
items: ItemViewModel[];
children: { [name: string]: TreeNode };
};

const roots: { [name: string]: TreeNode } = {};

const addToTree = (segments: string[], item: ItemViewModel) => {
const rootName = segments[0];
if (!roots[rootName])
roots[rootName] = { name: rootName, items: [], children: {} };
let node = roots[rootName];
const rest = segments.slice(1);
if (rest.length === 0) {
node.items.push(item);
return;
}
for (const seg of rest) {
if (!node.children[seg])
node.children[seg] = { name: seg, items: [], children: {} };
node = node.children[seg];
}
node.items.push(item);
};

items.forEach((item) => {
if (!item.parent || item.parent.length === 0) {
topLevelItems.push(item);
} else {
addToTree(item.parent, item);
}
});

const countSubtreeItems = (node: TreeNode): number =>
node.items.length +
Object.values(node.children).reduce((s, c) => s + countSubtreeItems(c), 0);

const renderNode = (node: TreeNode, path: string) => {
const cmp = compare[sortType];
return (
<li key={path}>
<details css={articleDetailsStyle} open>
<summary css={articleSummaryStyle}>
{node.name}
<span css={articleSectionTitleCountStyle}>
{countSubtreeItems(node)}
</span>
</summary>
<ul>
{Object.values(node.children)
.sort((a, b) => a.name.localeCompare(b.name))
.map((child) => renderNode(child, `${path}/${child.name}`))}

{node.items.sort(cmp).map((item) => (
<li key={item.items_show_path}>
<Link css={articlesListItemStyle} to={item.items_show_path}>
<MaterialSymbol
fill={item.modified && articleState !== "Draft"}
>
note
</MaterialSymbol>
<span css={articleListItemInnerStyle}>
{item.modified && articleState !== "Draft" && "(差分あり) "}
{item.title}
</span>
</Link>
</li>
))}
</ul>
</details>
</li>
);
};

return (
<details css={articleDetailsStyle} open={isDetailsOpen}>
<summary css={articleSummaryStyle} onClick={toggleAccordion}>
{ArticleState[articleState]}
<span css={articleSectionTitleCountStyle}>{items.length}</span>
</summary>
<ul>
{items.sort(compare[sortType]).map((item) => (
<li key={item.items_show_path}>
<Link css={articlesListItemStyle} to={item.items_show_path}>
<MaterialSymbol fill={item.modified && articleState !== "Draft"}>
note
</MaterialSymbol>
<span css={articleListItemInnerStyle}>
{item.modified && articleState !== "Draft" && "(差分あり) "}
{item.title}
</span>
</Link>
</li>
))}
{Object.values(roots)
.sort((a, b) => a.name.localeCompare(b.name))
.map((r) => renderNode(r, r.name))}

{topLevelItems.length > 0 &&
topLevelItems.sort(compare[sortType]).map((item) => (
<li key={item.items_show_path}>
<Link css={articlesListItemStyle} to={item.items_show_path}>
<MaterialSymbol
fill={item.modified && articleState !== "Draft"}
>
note
</MaterialSymbol>
<span css={articleListItemInnerStyle}>
{item.modified && articleState !== "Draft" && "(差分あり) "}
{item.title}
</span>
</Link>
</li>
))}
</ul>
</details>
);
Expand All @@ -93,6 +178,44 @@ const articleDetailsStyle = css({
"&[open] > summary::before": {
content: "'expand_more'",
},
// nested lists: draw vertical guide lines inside the padded area
"& ul": {
listStyle: "none",
margin: 0,
paddingLeft: getSpace(1),
},
"& ul ul": {
position: "relative",
paddingLeft: getSpace(3),
},
"& ul ul::before": {
content: "''",
position: "absolute",
left: getSpace(3),
top: 0,
bottom: 0,
width: 1,
backgroundColor: Colors.gray20,
},
"& ul ul > li": {
paddingLeft: getSpace(1.5),
},
"& ul ul ul": {
position: "relative",
paddingLeft: getSpace(4),
},
"& ul ul ul::before": {
content: "''",
position: "absolute",
left: getSpace(3),
top: 0,
bottom: 0,
width: 1,
backgroundColor: Colors.gray20,
},
"& ul ul ul > li": {
paddingLeft: getSpace(1.5),
},
});

const articleSummaryStyle = css({
Expand Down Expand Up @@ -137,9 +260,9 @@ const articlesListItemStyle = css({
fontSize: Typography.body2,
gap: getSpace(1),
lineHeight: LineHeight.bodyDense,
padding: `${getSpace(3 / 4)}px ${getSpace(5 / 2)}px ${getSpace(
3 / 4,
)}px ${getSpace(3 / 2)}px`,
padding: `${getSpace(3 / 4)}px ${getSpace(5 / 2)}px ${getSpace(3 / 4)}px ${getSpace(
3,
)}px`,
whiteSpace: "nowrap",
textOverflow: "ellipsis",

Expand Down
25 changes: 15 additions & 10 deletions src/lib/file-system-repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,17 +206,22 @@ export class FileSystemRepo {
}

private parseFilename(filename: string) {
return path.basename(filename, ".md");
return filename.split(".md")[0];
}

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

private async getItemFilenames(remote: boolean = false) {
return await fs.readdir(
this.getRootOrRemotePath(remote),
FileSystemRepo.fileSystemOptions(),
return (
await fs.readdir(
this.getRootOrRemotePath(remote),
FileSystemRepo.fileSystemOptions(),
)
).filter(
(itemFilename) =>
/\.md$/.test(itemFilename) && !itemFilename.startsWith(".remote/"),
);
}

Expand Down Expand Up @@ -246,6 +251,8 @@ export class FileSystemRepo {
private static fileSystemOptions() {
return {
encoding: "utf8",
withFileTypes: false,
recursive: true,
} as const;
}

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

const promises = itemFilenames
.filter((itemFilename) => /\.md$/.test(itemFilename))
.map(async (itemFilename) => {
const basename = this.parseFilename(itemFilename);
return await this.loadItemByBasename(basename);
});
const promises = itemFilenames.map(async (itemFilename) => {
const basename = this.parseFilename(itemFilename);
return await this.loadItemByBasename(basename);
});

const items = excludeNull(await Promise.all(promises));
return items;
Expand Down
1 change: 1 addition & 0 deletions src/lib/view-models/items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type ItemViewModel = {
title: string;
updated_at: string;
modified: boolean;
parent: string[];
};

export type ItemsIndexViewModel = {
Expand Down
1 change: 1 addition & 0 deletions src/server/api/items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const itemsIndex = async (req: Express.Request, res: Express.Response) => {
title: item.title,
updated_at: item.updatedAt,
modified: item.modified,
parent: item.name.split("/").slice(0, -1) || [],
};

if (item.id) {
Expand Down
5 changes: 4 additions & 1 deletion src/server/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ export function startLocalChangeWatcher({
watchPath: string;
}) {
const wsServer = new WebSocketServer({ server });
const watcher = chokidar.watch(watchPath);
const watcher = chokidar.watch(watchPath, {
ignored: /node_modules|\.git/,
persistent: true,
});
watcher.on("change", () => {
wsServer.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
Expand Down
34 changes: 17 additions & 17 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1315,10 +1315,10 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==

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

"@types/react@^19.1.13":
version "19.1.13"
Expand Down Expand Up @@ -5919,12 +5919,12 @@ raw-body@^3.0.0:
iconv-lite "0.6.3"
unpipe "1.0.0"

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

react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
Expand All @@ -5944,10 +5944,10 @@ react-router@^7.8.2:
cookie "^1.0.1"
set-cookie-parser "^2.6.0"

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

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

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

schema-utils@^4.3.0, schema-utils@^4.3.2:
version "4.3.2"
Expand Down
Loading