~repos /website

#astro#js#html#css

git clone https://pyrossh.dev/repos/website.git
Discussions: https://groups.google.com/g/rust-embed-devs

木 Personal website of pyrossh. Built with astrojs, shiki, vite.


4b5afa1b pyrossh

2 months ago
improve file-tree
src/components/RecursiveList.astro ADDED
@@ -0,0 +1,128 @@
1
+ ---
2
+ import prettyBytes from "pretty-bytes";
3
+
4
+ interface FileNode {
5
+ name: string;
6
+ path: string;
7
+ isDirectory: boolean;
8
+ children?: FileNode[];
9
+ size?: number;
10
+ ext?: string;
11
+ absolutePath?: string;
12
+ }
13
+
14
+ interface Props {
15
+ items: FileNode[];
16
+ repoTitle: string;
17
+ depth?: number;
18
+ }
19
+
20
+ const { items, repoTitle, depth = 0 } = Astro.props;
21
+ ---
22
+
23
+ {
24
+ items.map((item) => {
25
+ if (item.isDirectory && item.children) {
26
+ return (
27
+ <li class="folder" style={`padding-left: ${depth * 1}rem;`}>
28
+ <details>
29
+ <summary>
30
+ <span class="folder-name"> {item.name}/</span>
31
+ </summary>
32
+ <ul>
33
+ <Astro.self
34
+ items={item.children}
35
+ repoTitle={repoTitle}
36
+ depth={depth + 1}
37
+ />
38
+ </ul>
39
+ </details>
40
+ </li>
41
+ );
42
+ } else {
43
+ return (
44
+ <li class="file" style={`padding-left: ${depth * 1}rem;`}>
45
+ <div class="file-content">
46
+ <div class="name">
47
+ <a href={`/repos/${repoTitle}/files/${item.path}`} rel="nofollow">
48
+ {item.name}
49
+ </a>
50
+ </div>
51
+ {item.size && (
52
+ <div class="size">
53
+ <span>{prettyBytes(item.size).replace(" ", "")}</span>
54
+ </div>
55
+ )}
56
+ </div>
57
+ </li>
58
+ );
59
+ }
60
+ })
61
+ }
62
+
63
+ <style>
64
+ .folder,
65
+ .file {
66
+ margin: 0;
67
+ }
68
+
69
+ .folder details {
70
+ margin: 0.25rem 0;
71
+ }
72
+
73
+ summary {
74
+ display: flex;
75
+ align-items: center;
76
+ cursor: pointer;
77
+ padding: 0.4rem;
78
+ background-color: var(--color-box-bg);
79
+ }
80
+
81
+ .folder-name {
82
+ color: var(--color-link);
83
+ text-decoration: underline;
84
+ }
85
+
86
+ .folder ul {
87
+ padding: 0;
88
+ margin: 0.25rem 0 0.5rem 1rem;
89
+ }
90
+
91
+ .file {
92
+ margin: 0.25rem 0;
93
+ }
94
+
95
+ .file-content {
96
+ display: flex;
97
+ padding: 0.4rem;
98
+ background-color: var(--color-box-bg);
99
+ }
100
+
101
+ .file-icon {
102
+ display: inline-block;
103
+ width: 1.2rem;
104
+ margin-right: 0.5rem;
105
+ }
106
+
107
+ .name {
108
+ flex: 1;
109
+ display: flex;
110
+ align-items: center;
111
+ }
112
+
113
+ .name a {
114
+ text-decoration: none;
115
+ }
116
+
117
+ .name a:hover {
118
+ /* color: var(--color-link); */
119
+ text-decoration: underline;
120
+ text-underline-offset: 3px;
121
+ }
122
+
123
+ .size {
124
+ margin-left: auto;
125
+ padding-left: 1rem;
126
+ opacity: 0.7;
127
+ }
128
+ </style>
src/content.config.ts CHANGED
@@ -13,6 +13,72 @@ async function checkFileExists(filePath: string) {
13
13
  }
14
14
  }
15
15
 
16
+ interface FileNode {
17
+ name: string;
18
+ path: string;
19
+ isDirectory: boolean;
20
+ children?: FileNode[];
21
+ size?: number;
22
+ ext?: string;
23
+ absolutePath?: string;
24
+ }
25
+
26
+ function buildFileTree(files: any[]): FileNode[] {
27
+ const root: FileNode[] = [];
28
+
29
+ for (const file of files) {
30
+ const parts = file.name.split('/');
31
+ let currentLevel = root;
32
+
33
+ for (let i = 0; i < parts.length; i++) {
34
+ const part = parts[i];
35
+ const isLastPart = i === parts.length - 1;
36
+ const currentPath = parts.slice(0, i + 1).join('/');
37
+
38
+ let existingNode = currentLevel.find(node => node.name === part);
39
+
40
+ if (!existingNode) {
41
+ const newNode: FileNode = {
42
+ name: part,
43
+ path: currentPath,
44
+ isDirectory: !isLastPart,
45
+ };
46
+
47
+ if (isLastPart) {
48
+ // It's a file, add file metadata
49
+ newNode.size = file.size;
50
+ newNode.ext = file.ext;
51
+ newNode.absolutePath = file.absolutePath;
52
+ } else {
53
+ // It's a directory
54
+ newNode.children = [];
55
+ }
56
+
57
+ currentLevel.push(newNode);
58
+ existingNode = newNode;
59
+ }
60
+
61
+ if (!isLastPart) {
62
+ currentLevel = existingNode.children!;
63
+ }
64
+ }
65
+ }
66
+
67
+ return root;
68
+ }
69
+
70
+ const fileNodeSchema: z.ZodType<FileNode> = z.lazy(() =>
71
+ z.object({
72
+ name: z.string(),
73
+ path: z.string(),
74
+ isDirectory: z.boolean(),
75
+ children: z.array(fileNodeSchema),
76
+ size: z.number(),
77
+ ext: z.string(),
78
+ absolutePath: z.string(),
79
+ })
80
+ );
81
+
16
82
  export const collections = {
17
83
  repos: defineCollection({
18
84
  loader: {
@@ -25,12 +91,29 @@ export const collections = {
25
91
  const git = simpleGit(repoPath)
26
92
  const commits = await git.log(["--branches", "--tags"]);
27
93
  const paths = await git.raw(["ls-files"]);
94
+ // await git.log(["-p", "--", "file"]); // to get diffs
95
+ // await git.log(["file"]);
28
96
  const files = await Promise.all(paths.split("\n")
29
97
  .filter((p) => p.length > 0)
30
98
  .map((p) => fs.stat(`${repoPath}/${p}`)
31
99
  .then((stat) => ({ name: p, absolutePath: `${repoPath}/${p}`, ...stat }))
32
100
  .catch(() => null)
33
- ))
101
+ ));
102
+ const sortAll = (a: FileNode, b: FileNode): number => {
103
+ if (a.isDirectory && !b.isDirectory) return -1;
104
+ if (!a.isDirectory && b.isDirectory) return 1;
105
+ return a.name.localeCompare(b.name);
106
+ }
107
+ const sortChildren = (nodes: FileNode[]) => {
108
+ for (const node of nodes) {
109
+ if (node.isDirectory && node.children) {
110
+ node.children.sort(sortAll);
111
+ sortChildren(node.children);
112
+ }
113
+ }
114
+ return nodes.sort(sortAll);
115
+ };
116
+ const fileTree = sortChildren(buildFileTree(files.filter((f) => f !== null)));
34
117
  const readmeContent = await checkFileExists(readmePath) ? await renderMarkdown(await fs.readFile(readmePath, 'utf-8')) : {
35
118
  html: '',
36
119
  metadata: {},
@@ -51,7 +134,7 @@ export const collections = {
51
134
  branches: item.refs.split(",").filter((ref: string) => ref.includes("origin/")).map((ref) => ref.replace("origin/", "")),
52
135
  tags: item.refs.split(",").filter((ref: string) => ref.includes("tag: ")).map((ref) => ref.replace("tag: ", "")),
53
136
  })),
54
- files: files.filter((f) => f !== null),
137
+ files: fileTree,
55
138
  },
56
139
  rendered: readmeContent,
57
140
  });
@@ -77,13 +160,7 @@ export const collections = {
77
160
  tags: z.array(z.string()),
78
161
  // diff: z.string(),
79
162
  })),
80
- files: z.array(z.object({
163
+ files: z.array(fileNodeSchema),
81
- name: z.string(),
82
- size: z.number(),
83
- ext: z.string(),
84
- isDirectory: z.boolean(),
85
- content: z.string(),
86
- })),
87
164
  }),
88
165
  }),
89
166
  content: defineCollection({
src/layouts/Base.css CHANGED
@@ -12,8 +12,6 @@
12
12
  --color-code-bg: light-dark(hsl(0, 0%, 95%), hsl(0, 0%, 0%));
13
13
  --color-pre-bg: light-dark(hsl(0, 0%, 95%), hsl(0, 0%, 0%));
14
14
  --color-tag: light-dark(hsl(152, 96%, 35%), hsl(152, 96%, 44%));
15
- --color-row-odd: light-dark(hsl(0, 0%, 94%), hsl(0, 0%, 10%));
16
- --color-row-even: light-dark(hsl(0, 0%, 88%), hsl(0, 0%, 7%));
17
15
  --btn-light: none;
18
16
  --btn-dark: block;
19
17
 
src/layouts/Repo.astro CHANGED
@@ -160,9 +160,9 @@ const { pathname } = Astro.url;
160
160
  }
161
161
 
162
162
  a {
163
- font-size: 12pt;
163
+ font-size: 1rem;
164
+ font-weight: 500;
164
165
  padding-right: 0.5rem;
165
- color: #3395ff;
166
166
  cursor: pointer;
167
167
 
168
168
  &[aria-current="true"] {
src/pages/repos/[...slug]/files/[...file]/index.astro CHANGED
@@ -4,23 +4,26 @@ import path from "path";
4
4
  import { type CollectionEntry, getCollection } from "astro:content";
5
5
  import { Code } from "astro-expressive-code/components";
6
6
  import RepoLayout from "@/layouts/Repo.astro";
7
+ import { collectFiles } from "@/utils/files";
7
8
 
8
9
  export async function getStaticPaths() {
9
10
  const repos = await getCollection("repos");
10
- return repos.flatMap((repo) =>
11
+ return repos.flatMap((repo) => {
12
+ const allFiles = collectFiles(repo.data.files);
11
- repo.data.files.map((file) => ({
13
+ return allFiles.map((file) => ({
12
- params: { slug: repo.id, file: file.name },
14
+ params: { slug: repo.id, file: file.path },
13
15
  props: {
14
16
  repo: repo,
15
17
  file: file,
16
18
  },
17
- }))
19
+ }));
18
- );
20
+ });
19
21
  }
20
22
  type Props = {
21
23
  repo: CollectionEntry<"repos">;
22
24
  file: {
23
25
  name: string;
26
+ path: string;
24
27
  size: number;
25
28
  isDirectory: boolean;
26
29
  absolutePath: string;
@@ -53,14 +56,14 @@ const isImage = [
53
56
  <div>
54
57
  <div class="title">
55
58
  <span>file:</span>
56
- <h3>{file.name}</h3>
59
+ <h3>{file.path}</h3>
57
60
  </div>
58
61
  {isBinary || isLarge ? (
59
62
  <p>The file is too large to be displayed ({file.size} bytes).</p>
60
63
  ) : isImage ? (
61
64
  <img
62
65
  src={`data:image/${ext};base64,${Buffer.from(contentBuffer).toString("base64")}`}
63
- alt={file.name}
66
+ alt={file.path}
64
67
  />
65
68
  ) : (
66
69
  <Code
@@ -83,6 +86,7 @@ const isImage = [
83
86
  margin-top: 0.75rem;
84
87
  margin-bottom: 0.5rem;
85
88
  font-size: 1.1rem;
89
+ font-weight: 500;
86
90
  }
87
91
  span {
88
92
  font-weight: 600;
src/pages/repos/[...slug]/files/index.astro CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  import { type CollectionEntry, getCollection } from "astro:content";
3
- import prettyBytes from "pretty-bytes";
4
3
  import RepoLayout from "@/layouts/Repo.astro";
4
+ import RecursiveList from "@/components/RecursiveList.astro";
5
5
 
6
6
  export async function getStaticPaths() {
7
7
  const repos = await getCollection("repos");
@@ -18,59 +18,24 @@ const {
18
18
 
19
19
  <RepoLayout data={Astro.props.data}>
20
20
  <div class="container">
21
- <ul>
22
- {
23
- files.map((file) => (
24
- <li>
25
- <div class="name">
26
- <a href={`/repos/${title}/files/${file.name}`} rel="nofollow">
27
- {file.name}
28
- </a>
29
- </div>
30
- <div class="size">
21
+ <ul class="file-tree">
31
- <span>{prettyBytes(file.size).replace(" ", "")}</span>
22
+ <RecursiveList items={files} repoTitle={title} />
32
- </div>
33
- </li>
34
- ))
35
- }
36
23
  </ul>
37
24
  </div>
38
25
  </RepoLayout>
39
26
  <style>
40
27
  .container {
41
28
  display: flex;
42
- margin-top: 1rem;
29
+ margin-top: 0.25rem;
43
30
  }
44
31
 
45
- ul {
32
+ .file-tree {
46
33
  display: flex;
47
34
  flex: 1;
48
35
  flex-direction: column;
49
- font-size: 1.1rem;
36
+ font-size: 0.95rem;
50
- gap: 0.5rem;
37
+ font-weight: 500;
51
-
52
- li {
53
- display: flex;
38
+ list-style: none;
54
- flex: 1;
55
- flex-wrap: wrap;
56
-
57
- &:nth-child(odd) {
58
- background-color: var(--color-row-odd);
59
- }
60
-
61
- &:nth-child(even) {
62
- background-color: var(--color-row-even);
63
- }
64
- }
65
- }
66
-
67
- .name {
68
- flex: 1;
69
- margin-left: 0.5rem;
39
+ margin: 0;
70
- }
71
-
72
- .size {
73
- margin-right: 0.5rem;
74
- /* background-color: var(--color-code-bg); */
75
40
  }
76
41
  </style>
src/pages/repos/[...slug]/index.astro CHANGED
@@ -15,7 +15,7 @@ export async function getStaticPaths() {
15
15
  type Props = CollectionEntry<"repos">;
16
16
 
17
17
  const {
18
- data: { commits },
18
+ data: { title, commits },
19
19
  } = Astro.props;
20
20
  const latestCommits = commits.slice(0, 3);
21
21
  const { Content } = await render(Astro.props);
@@ -23,7 +23,7 @@ const { Content } = await render(Astro.props);
23
23
 
24
24
  <RepoLayout data={Astro.props.data}>
25
25
  <div class={styles.summary}>
26
- {latestCommits.map((commit) => <Commit {...commit} />)}
26
+ {latestCommits.map((commit) => <Commit title={title} {...commit} />)}
27
27
  </div>
28
28
  <hr />
29
29
  <article>
src/utils/files.ts ADDED
@@ -0,0 +1,23 @@
1
+ interface FileNode {
2
+ name: string;
3
+ path: string;
4
+ isDirectory: boolean;
5
+ children?: FileNode[];
6
+ size?: number;
7
+ ext?: string;
8
+ absolutePath?: string;
9
+ }
10
+
11
+ export const collectFiles = (nodes: FileNode[]): FileNode[] => {
12
+ const files: FileNode[] = [];
13
+
14
+ for (const node of nodes) {
15
+ if (node.isDirectory && node.children) {
16
+ files.push(...collectFiles(node.children));
17
+ } else if (!node.isDirectory) {
18
+ files.push(node);
19
+ }
20
+ }
21
+
22
+ return files;
23
+ };