diff --git a/contents/articles/README.md b/contents/doc/article.md similarity index 99% rename from contents/articles/README.md rename to contents/doc/article.md index d1f7a8a4..bacdd767 100644 --- a/contents/articles/README.md +++ b/contents/doc/article.md @@ -1,3 +1,4 @@ + # 記事ページ ## frontmatter diff --git a/contents/members/README.md b/contents/doc/members.md similarity index 95% rename from contents/members/README.md rename to contents/doc/members.md index 0cfe1325..476026d9 100644 --- a/contents/members/README.md +++ b/contents/doc/members.md @@ -1,3 +1,4 @@ + # メンバーページ [`[member].astro`](/src/pages/members/[member].astro) によってレンダリングされるメンバー詳細ページです。 @@ -10,10 +11,11 @@ | ---------------- | ---- | ------------------------------------------------------------------------------------------------------------- | | `nameJa` | ✅ | 名前。 | | `nameEn` | ✅ | 英語名。 | -| `joinYear` | ✅ | ut.code(); に入会した年。ソートに使用しています。。 | +| `joinYear` | ✅ | ut.code(); に入会した年。ソートに使用しています。 | | `description` | ✅ | 今の自分を表す一言。 | | `faceImage` | ✅ | 顔写真。縦横比は 1:1。顔を中央に配置し、顔の横幅がおよそ写真の横幅の 1/2 になるようにトリミングしてください。 | | `upperBodyImage` | ✅ | バストショット。縦横比は 1:1。 | | `github` | | GitHub の**アカウント名** | | `twitter` | | X (旧 Twitter) の **ID**。 | | `website` | | 個人のウェブサイトの URL。 | + diff --git a/contents/doc/projects.md b/contents/doc/projects.md new file mode 100644 index 00000000..85109d19 --- /dev/null +++ b/contents/doc/projects.md @@ -0,0 +1,33 @@ +# プロジェクトページ + +`src/pages/projects/[...id].astro` によってレンダリングされるプロジェクト詳細ページです。 + +プロジェクトには 3 種類あります。 + +- 長期プロジェクト +- 学園祭プロジェクト +- ハッカソン + +歴史的な経緯で長期プロジェクトは `contents/projects` 直下にありますが、学園祭プロジェクトとハッカソンは `contents/projects/{festival,hackathon}`にいれるようにしています。 + +## frontmatter + +| キー | 必須 | 説明 | +| ------------- | ---- | ------------------------------------------------------------------------------ | +| `title` | ✅ | プロジェクト名 | +| `order` | | 表示順。指定されなかった場合は `date` 降順でソートされます。 | +| `date` | ✅ | プロジェクトの開始日。現状ソートのみで利用しています。 | +| `image` | ✅ | イメージ画像に関するデータ。 | +| `image.src` | ✅ | イメージファイルへの markdown からの相対パス。 | +| `image.fit` | | イメージのクロップ方法。 default = "cover"。 | +| `image.bg` | | イメージの背景色。ロード中と `crop` = "contain" のときの背景に使われています。 | +| `description` | ✅ | 短い説明。 | +| `tags` | | 使用されている技術など。現状タグごとのフィルタリング機能等は提供していません。 | +| `github` | | プロジェクトの GitHub 上での URL。 | +| `youtube` | | プロジェクトの YouTube 上での URL。 | +| `website` | | プロジェクトのウェブサイトの URL。 | + +## body について + +長期プロジェクトの body は各長期プロジェクトのページに使用されています。 +それ以外は必要ありません。 diff --git a/contents/project-kinds.ts b/contents/project-kinds.ts new file mode 100644 index 00000000..d9cdfbf6 --- /dev/null +++ b/contents/project-kinds.ts @@ -0,0 +1,25 @@ +export const kinds = [ + { + frontmatter: "long-term", + path: undefined, + title: "長期プロジェクト", + description: + "ut.code(); で長期間にわたって開発を行っているプロジェクトです", + tabTitle: "長期プロジェクト", + }, + { + frontmatter: "festival", + path: "festival", + title: "学園祭プロジェクト", + description: + "ut.code(); が毎年参加している、学園祭に展示するプロジェクトです", + tabTitle: "学園祭", + }, + { + frontmatter: "hackathon", + path: "hackathon", + title: "ハッカソンプロジェクト", + description: "ut.code(); で短期間で開発したハッカソンのプロジェクトです", + tabTitle: "ハッカソン", + }, +] as const; diff --git a/contents/projects/README.md b/contents/projects/README.md deleted file mode 100644 index 81e74b34..00000000 --- a/contents/projects/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# プロジェクトページ - -[`project.tsx`](/src/templates/project.tsx) によってレンダリングされるプロジェクト詳細ページです。 - -## frontmatter - -| キー | 必須 | 説明 | -| ------------- | ---- | ------------------------------------------------------------------------------ | -| `title` | ✅ | プロジェクト名 | -| `slug` | ✅ | ウェブサイト上の URL で使用される文字列 (重複不可) | -| `order` | | 表示順。指定されなかった場合は `date` 降順でソートされます。 | -| `date` | ✅ | プロジェクトの開始日。現状ソートのみで利用しています。 | -| `image` | ✅ | イメージ画像。 | -| `description` | ✅ | 短い説明。 | -| `tags` | ✅ | 使用されている技術など。現状タグごとのフィルタリング機能等は提供していません。 | -| `github` | | プロジェクトの GitHub 上での URL。 | -| `youtube` | | プロジェクトの YouTube 上での URL。 | -| `website` | | プロジェクトのウェブサイトの URL。 | - -- URL とアイコンを追加したい場合は、 [`project.tsx`](/src/templates/project.tsx) のアイコン表示部分と、末尾にある query を編集する必要があります diff --git a/contents/projects/hackathon/2023-08-17/call-paper.md b/contents/projects/hackathon/2023-08-17/call-paper.md new file mode 100644 index 00000000..ce0a7459 --- /dev/null +++ b/contents/projects/hackathon/2023-08-17/call-paper.md @@ -0,0 +1,11 @@ +--- +title: Call Paper +date: 2023-08-17 +kind: hackathon +status: finished +image: + src: ./call-paper.png +description: 論文の引用関係を可視化できるアプリ +github: https://github.com/ut-code/call-paper +website: https://call-paper.pages.dev +--- diff --git a/contents/projects/hackathon/2023-08-17/call-paper.png b/contents/projects/hackathon/2023-08-17/call-paper.png new file mode 100644 index 00000000..b47f6a98 Binary files /dev/null and b/contents/projects/hackathon/2023-08-17/call-paper.png differ diff --git a/contents/projects/hackathon/2023-08-17/denigma.md b/contents/projects/hackathon/2023-08-17/denigma.md new file mode 100644 index 00000000..9941c008 --- /dev/null +++ b/contents/projects/hackathon/2023-08-17/denigma.md @@ -0,0 +1,11 @@ +--- +title: Denigma +date: 2023-08-17 +kind: hackathon +status: dead +image: + src: ./denigma.png +description: "暗号解読ゲーム。シーザー暗号やRSA暗号など、基本的な暗号に触れてみよう!" +github: https://github.com/ut-code/denigma +website: https://utcode-denigma.onrender.com/ +--- diff --git a/contents/projects/hackathon/2023-08-17/denigma.png b/contents/projects/hackathon/2023-08-17/denigma.png new file mode 100644 index 00000000..0be198e7 Binary files /dev/null and b/contents/projects/hackathon/2023-08-17/denigma.png differ diff --git a/contents/projects/hackathon/2023-08-17/music-app.md b/contents/projects/hackathon/2023-08-17/music-app.md new file mode 100644 index 00000000..282a68b2 --- /dev/null +++ b/contents/projects/hackathon/2023-08-17/music-app.md @@ -0,0 +1,11 @@ +--- +title: music-app +date: 2023-08-17 +kind: hackathon +status: finished +image: + src: ./music-app.png +description: その日の気分にあった作業用BGMを生成してくれるアプリ +github: https://github.com/ut-code/music-app +website: https://ut-code.github.io/music-app +--- diff --git a/contents/projects/hackathon/2023-08-17/music-app.png b/contents/projects/hackathon/2023-08-17/music-app.png new file mode 100644 index 00000000..5831e36a Binary files /dev/null and b/contents/projects/hackathon/2023-08-17/music-app.png differ diff --git a/contents/projects/hackathon/2023-08-17/todo.md b/contents/projects/hackathon/2023-08-17/todo.md new file mode 100644 index 00000000..62d626a1 --- /dev/null +++ b/contents/projects/hackathon/2023-08-17/todo.md @@ -0,0 +1,10 @@ +--- +title: todoアプリ +date: 2023-08-17 +kind: hackathon +status: finished +image: + src: ./todo.png +description: ToDoを管理できる Web アプリ +github: https://github.com/ut-code/todo-2023-hackathon +--- diff --git a/contents/projects/hackathon/2023-08-17/todo.png b/contents/projects/hackathon/2023-08-17/todo.png new file mode 100644 index 00000000..182a6036 Binary files /dev/null and b/contents/projects/hackathon/2023-08-17/todo.png differ diff --git a/contents/projects/hackathon/2024-06-08/bowling.md b/contents/projects/hackathon/2024-06-08/bowling.md new file mode 100644 index 00000000..92210136 --- /dev/null +++ b/contents/projects/hackathon/2024-06-08/bowling.md @@ -0,0 +1,11 @@ +--- +title: 壁よけボウリング +kind: hackathon +status: finished +date: 2024-06-08 +description: 障害物を避けてピンを倒そう! どこか懐かしくてどこか新感覚のボウリングゲームをお楽しみあれ! +image: + src: ./bowling.png +website: https://ut-code.github.io/bowling-app/ +github: https://github.com/ut-code/bowling-app +--- diff --git a/contents/projects/hackathon/2024-06-08/bowling.png b/contents/projects/hackathon/2024-06-08/bowling.png new file mode 100644 index 00000000..6b119126 Binary files /dev/null and b/contents/projects/hackathon/2024-06-08/bowling.png differ diff --git a/contents/projects/hackathon/2024-06-08/shift-syncer.md b/contents/projects/hackathon/2024-06-08/shift-syncer.md new file mode 100644 index 00000000..2df16862 --- /dev/null +++ b/contents/projects/hackathon/2024-06-08/shift-syncer.md @@ -0,0 +1,10 @@ +--- +title: ShiftSyncer +kind: hackathon +status: finished +date: 2024-06-08 +description: 複数人から登録されたシフトの希望を基に、最適なシフトを作成するアプリです。シフトの希望を出す際に、シフトに入れるか否かだけではなく、積極的に入りたいのか、それとも人が居なかったら入っても良い程度なのかといった事情も勘案できるようになっています。 +image: + src: ./shift-syncer.png +github: https://github.com/ut-code/ShiftSyncer +--- diff --git a/contents/projects/hackathon/2024-06-08/shift-syncer.png b/contents/projects/hackathon/2024-06-08/shift-syncer.png new file mode 100644 index 00000000..2de273b0 Binary files /dev/null and b/contents/projects/hackathon/2024-06-08/shift-syncer.png differ diff --git a/contents/projects/hackathon/2024-06-08/typing-script.md b/contents/projects/hackathon/2024-06-08/typing-script.md new file mode 100644 index 00000000..fa188749 --- /dev/null +++ b/contents/projects/hackathon/2024-06-08/typing-script.md @@ -0,0 +1,11 @@ +--- +title: TypingScript +kind: hackathon +status: finished +date: 2024-06-08 +description: ソースコードに特化したタイピング練習サイトです。 +image: + src: ./typing-script.png +website: https://ut-code.github.io/TypingScript +github: https://github.com/ut-code/TypingScript +--- diff --git a/contents/projects/hackathon/2024-06-08/typing-script.png b/contents/projects/hackathon/2024-06-08/typing-script.png new file mode 100644 index 00000000..72720c34 Binary files /dev/null and b/contents/projects/hackathon/2024-06-08/typing-script.png differ diff --git a/contents/projects/hackathon/hackathon-template.md_ b/contents/projects/hackathon/hackathon-template.md_ new file mode 100644 index 00000000..dc9921e3 --- /dev/null +++ b/contents/projects/hackathon/hackathon-template.md_ @@ -0,0 +1,11 @@ +--- +title: Title +date: hackathon-start-date +kind: hackathon +status: finished +image: + src: ./thumbnail.png +description: "簡単なタワーディフェンスゲームです。自機も移動して攻撃すると言う特徴があります。" +github: https://github.com/ut-code/your-project +website: https://your-project.pages.dev +--- diff --git a/contents/projects/hackathon/tower-battle/index.md b/contents/projects/hackathon/tower-battle/index.md new file mode 100644 index 00000000..8cc0c600 --- /dev/null +++ b/contents/projects/hackathon/tower-battle/index.md @@ -0,0 +1,18 @@ +--- +title: Tower Battle +date: 2025-02-24 +kind: hackathon +status: stable +image: + src: ./thumbnail.png +description: "簡単なタワーディフェンスゲームです。自機も移動して攻撃すると言う特徴があります。" +members: + - ykobayashi +tags: + - TypeScript + - Svelte + - DaisyUI + - Cloudflare +github: https://github.com/aster-void/tower-battle +website: https://tower-d5g.pages.dev +--- diff --git a/contents/projects/hackathon/tower-battle/thumbnail.png b/contents/projects/hackathon/tower-battle/thumbnail.png new file mode 100644 index 00000000..9b94b341 Binary files /dev/null and b/contents/projects/hackathon/tower-battle/thumbnail.png differ diff --git a/src/components/JoinUsCTA.astro b/src/components/JoinUsCTA.astro index c7b4ccb3..4757188f 100644 --- a/src/components/JoinUsCTA.astro +++ b/src/components/JoinUsCTA.astro @@ -10,7 +10,7 @@ const props = Astro.props;

- 初めは誰もが未経験です。まずはお気軽にお声掛けください! + 始めは誰もが未経験です。まずはお気軽にお声掛けください!

ut.code(); に参加する
diff --git a/src/content.config.ts b/src/content.config.ts index d2791370..e10a98b1 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -19,8 +19,12 @@ const members = defineCollection({ schema: CreateMemberSchema, }); const projects = defineCollection({ - loader: glob({ base: "./contents/projects", pattern: "*/index.{md,mdx}" }), + loader: glob({ base: "./contents/projects", pattern: "**/*.{md,mdx}" }), schema: CreateProjectSchema, }); -export const collections = { articles, members, projects }; +export const collections = { + articles, + members, + projects, +}; diff --git a/src/pages/articles/[...id].astro b/src/pages/articles/[...id].astro index 7d14a75a..b1c58cf8 100644 --- a/src/pages/articles/[...id].astro +++ b/src/pages/articles/[...id].astro @@ -1,13 +1,14 @@ --- import { Picture } from "astro:assets"; -import { getCollection, getEntry, render } from "astro:content"; +import { render } from "astro:content"; import JoinUsCTA from "+/components/JoinUsCTA.astro"; import GlobalLayout from "+/layouts/GlobalLayout.astro"; -import { Focus } from "+/schema"; +import { getArticles, getMember } from "+/query.ts"; +import { Focus } from "+/schema.ts"; import { format } from "date-fns"; export async function getStaticPaths() { - const articles = await getCollection("articles"); + const articles = await getArticles(); return articles.map((article) => ({ params: { id: article.id }, props: { article }, @@ -16,12 +17,7 @@ export async function getStaticPaths() { const { article } = Astro.props; const { Content } = await render(article); -const author = - article.data.author && (await getEntry("members", article.data.author.id)); -if (article.data.author && !author) - throw new Error( - `Author not found for article ${article.id}, searched for author ${article.data.author.id}`, - ); +const author = article.data.author && (await getMember(article.data.author.id)); --- { - const articles = (await getCollection("articles")).sort( - (a, b) => b.data.date.getTime() - a.data.date.getTime(), - ); + const articles = await getArticles(); return paginate(articles, { pageSize: 18 }); }) satisfies GetStaticPaths; diff --git a/src/pages/index.astro b/src/pages/index.astro index 8a98fc3d..b356d59d 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,6 +1,5 @@ --- import { Picture } from "astro:assets"; -import { getCollection } from "astro:content"; import { Icon } from "astro-icon/components"; import GlobalLayout from "+/layouts/GlobalLayout.astro"; @@ -9,25 +8,17 @@ import ActionButton from "+/components/ActionButton.astro"; import ArticleList from "+/components/ArticleList.astro"; import ProjectList from "+/components/ProjectList.astro"; import SectionHeader from "+/components/per-page/SectionHeader.astro"; +import { getArticles, getProjects } from "+/query.ts"; import { Focus } from "+/schema.ts"; import LogoGMOMedia from "+/images/donators/gmo-media.png"; import Hero from "+/images/headers/index.jpg"; import LabCafe from "+/images/lab-cafe.jpg"; -const projects = (await getCollection("projects")).sort((a, b) => { - const a_order = a.data.order ?? Number.POSITIVE_INFINITY; - const b_order = b.data.order ?? Number.POSITIVE_INFINITY; - if (a_order !== b_order) { - return a_order - b_order; - } - return b.data.date.getTime() - a.data.date.getTime(); -}); +const projects = await getProjects("long-term"); projects.splice(6); -const articles = (await getCollection("articles")).sort( - (a, b) => b.data.date.getTime() - a.data.date.getTime(), -); +const articles = await getArticles(); articles.splice(3); --- diff --git a/src/pages/members.astro b/src/pages/members.astro index 6a673b5d..8ed158b0 100644 --- a/src/pages/members.astro +++ b/src/pages/members.astro @@ -1,14 +1,12 @@ --- import { Picture } from "astro:assets"; -import { getCollection } from "astro:content"; import JoinUsCTA from "+/components/JoinUsCTA.astro"; import Header from "+/images/headers/members.jpg"; import GlobalLayout from "+/layouts/GlobalLayout.astro"; -import { Focus } from "+/schema"; +import { getMembers } from "+/query.ts"; +import { Focus } from "+/schema.ts"; -const members = (await getCollection("members")).sort( - (a, b) => b.data.joinYear - a.data.joinYear, -); +const members = await getMembers(); --- ({ params: { member: member.id }, props: { member }, @@ -18,11 +19,9 @@ export async function getStaticPaths() { const { member } = Astro.props; const { Content } = await render(member); -const articles = await getCollection( - "articles", - (article) => article.data.author?.id === member.id, -); -articles.sort((a, b) => b.data.date.getTime() - a.data.date.getTime()); +const articles = await getArticles({ + where: (article) => article.data.author?.id === member.id, +}); --- b.data.date.getTime() - a.data.date.getTime()); >
b.data.date.getTime() - a.data.date.getTime()); rel="noreferrer" aria-label="GitHubを見る" > - + ) } diff --git a/src/pages/projects.astro b/src/pages/projects.astro deleted file mode 100644 index 78a6e747..00000000 --- a/src/pages/projects.astro +++ /dev/null @@ -1,29 +0,0 @@ ---- -import { getCollection } from "astro:content"; -import ProjectList from "+/components/ProjectList.astro"; -import GlobalLayout from "+/layouts/GlobalLayout.astro"; -import { Focus } from "+/schema"; - -const projects = (await getCollection("projects")).sort((a, b) => { - const a_order = a.data.order ?? Number.POSITIVE_INFINITY; - const b_order = b.data.order ?? Number.POSITIVE_INFINITY; - if (a_order !== b_order) { - return a_order - b_order; - } - return b.data.date.getTime() - a.data.date.getTime(); -}); ---- - - -
-
-

プロジェクト

- -
-
-
diff --git a/src/pages/projects/[project].astro b/src/pages/projects/[...id].astro similarity index 68% rename from src/pages/projects/[project].astro rename to src/pages/projects/[...id].astro index 59caed96..180c0981 100644 --- a/src/pages/projects/[project].astro +++ b/src/pages/projects/[...id].astro @@ -1,18 +1,18 @@ --- import { Picture } from "astro:assets"; -import { getCollection } from "astro:content"; import { render } from "astro:content"; import ActionButton from "+/components/ActionButton.astro"; import JoinUsCTA from "+/components/JoinUsCTA.astro"; import GlobalLayout from "+/layouts/GlobalLayout.astro"; -import { Focus, type Project } from "+/schema"; +import { getProjects } from "+/query"; +import { Focus } from "+/schema.ts"; import type { GetStaticPaths } from "astro"; import { Icon } from "astro-icon/components"; export const getStaticPaths = (async () => { - const projects = await getCollection("projects"); + const projects = await getProjects("all"); return projects.map((project) => ({ - params: { project: project.id }, + params: { id: project.id }, props: { project }, })); }) satisfies GetStaticPaths; @@ -26,7 +26,7 @@ const { Content } = await render(project); focus={Focus.projects} image={project.data.image.src} > -
+
@@ -86,23 +86,53 @@ const { Content } = await render(project); }
{ - project.data.website && ( + project.data.website && project.data.status !== "dead" && ( - {project.data.title} へ + {project.data.title} へ ) }
-
-
- {project.data.description} +
+ { + project.data.kind === "long-term" && ( +
+ {project.data.description} +
+ ) + } +
+ { + project.data.kind === "long-term" ? ( + + ) : ( + project.data.description + ) + }
-
-
+
- - ut.code(); で{project.data.title}を開発しませんか? - + { + project.data.kind === "long-term" && ( + + ut.code(); で{project.data.title}を開発しませんか? + + ) + } + { + project.data.kind === "hackathon" && ( + + ut.code(); では、定期的にハッカソンイベントを行っています。 + + ) + } + { + project.data.kind === "festival" && ( + + ut.code(); は、毎年五月祭・駒場祭に参加しています。 + + ) + }
diff --git a/src/pages/projects/[...kind].astro b/src/pages/projects/[...kind].astro new file mode 100644 index 00000000..2e9e6fc7 --- /dev/null +++ b/src/pages/projects/[...kind].astro @@ -0,0 +1,65 @@ +--- +import ProjectList from "+/components/ProjectList.astro"; +import GlobalLayout from "+/layouts/GlobalLayout.astro"; +import { getProjects } from "+/query"; +import { Focus } from "+/schema"; +import { kinds } from "+contents/project-kinds.ts"; + +export function getStaticPaths() { + return kinds.map((kind) => ({ + params: { kind: kind.path }, + props: { kind }, + })); +} + +const kind = Astro.props.kind; + +const projects = await getProjects(kind.frontmatter); +--- + + +
+

{kind.title}

+
+ { + kinds + .filter( + (kind) => + kind.path !== "festival" /* TODO: migrate festival projects */, + ) + .map((tab) => ( + + + {tab.tabTitle} + + + )) + } +
+ { + projects.length === 0 ? ( +

+ プロジェクトはまだ反映されていません。 +

+ ) : ( + + ) + } + { + projects.length < 10 && ( +

+ このページは準備中です。ほとんどのプロジェクトは反映されていません。 +

+ ) + } +
+
diff --git a/src/query.ts b/src/query.ts new file mode 100644 index 00000000..54afacf3 --- /dev/null +++ b/src/query.ts @@ -0,0 +1,39 @@ +import { type CollectionEntry, getCollection, getEntry } from "astro:content"; +import type { Kind } from "./schema.ts"; + +export async function getProjects(kind: Kind | "all") { + return (await getCollection("projects")) + .filter((project) => kind === "all" || project.data.kind === kind) + .sort((a, b) => { + const a_order = a.data.order ?? Number.POSITIVE_INFINITY; + const b_order = b.data.order ?? Number.POSITIVE_INFINITY; + if (a_order !== b_order) { + return a_order - b_order; + } + return b.data.date.getTime() - a.data.date.getTime(); + }); +} + +export async function getArticles(options?: { + where?: (article: CollectionEntry<"articles">) => boolean; +}) { + let articles = (await getCollection("articles")).sort( + (a, b) => b.data.date.getTime() - a.data.date.getTime(), + ); + if (options?.where) { + articles = articles.filter(options.where); + } + return articles; +} + +export async function getMembers() { + const members = await getCollection("members"); + return members.sort((m1, m2) => m2.data.joinYear - m1.data.joinYear); +} + +export async function getMember(id: string) { + const member = await getEntry("members", id); + if (!member) + throw new Error(`member search error: member not found for ${id}`); + return member; +} diff --git a/src/schema.ts b/src/schema.ts index e98b2ed7..b7443b7c 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1,5 +1,6 @@ import { type ImageFunction, reference } from "astro:content"; import { z } from "astro:schema"; +import { kinds } from "+contents/project-kinds"; import { TZDate } from "@date-fns/tz"; export type Article = z.infer>; @@ -33,10 +34,11 @@ export const CreateArticleSchema = ({ image }: { image: ImageFunction }) => }, ); +export type Kind = (typeof kinds)[number]["frontmatter"]; export const CreateProjectSchema = ({ image }: { image: ImageFunction }) => z.object({ title: z.string(), - kind: z.enum(["long-term", "hackathon", "festival"]), + kind: z.enum(kinds.map((kind) => kind.frontmatter) as [Kind, ...Kind[]]), status: z.enum([ "plan", "under-development", @@ -53,7 +55,7 @@ export const CreateProjectSchema = ({ image }: { image: ImageFunction }) => bg: z.string().optional().default("whitesmoke"), }), description: z.string(), - tags: z.array(z.string()), + tags: z.array(z.string()).optional().default([]), github: z.string().url().optional(), youtube: z.string().url().optional(), website: z.string().url().optional(),