♻️ Use keystonejs
This commit is contained in:
		
							
								
								
									
										1
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| PUBLIC_CMS="http://localhost:3000" | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -21,5 +21,4 @@ pnpm-debug.log* | ||||
| .DS_Store | ||||
|  | ||||
| # Development content | ||||
| content | ||||
| public/media | ||||
							
								
								
									
										4
									
								
								content/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								content/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| node_modules | ||||
| .keystone/admin | ||||
| keystone.db | ||||
| *.log | ||||
							
								
								
									
										488
									
								
								content/.keystone/config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										488
									
								
								content/.keystone/config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,488 @@ | ||||
| "use strict"; | ||||
| var __defProp = Object.defineProperty; | ||||
| var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | ||||
| var __getOwnPropNames = Object.getOwnPropertyNames; | ||||
| var __hasOwnProp = Object.prototype.hasOwnProperty; | ||||
| var __export = (target, all) => { | ||||
|   for (var name in all) | ||||
|     __defProp(target, name, { get: all[name], enumerable: true }); | ||||
| }; | ||||
| var __copyProps = (to, from, except, desc) => { | ||||
|   if (from && typeof from === "object" || typeof from === "function") { | ||||
|     for (let key of __getOwnPropNames(from)) | ||||
|       if (!__hasOwnProp.call(to, key) && key !== except) | ||||
|         __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); | ||||
|   } | ||||
|   return to; | ||||
| }; | ||||
| var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); | ||||
|  | ||||
| // keystone.ts | ||||
| var keystone_exports = {}; | ||||
| __export(keystone_exports, { | ||||
|   default: () => keystone_default | ||||
| }); | ||||
| module.exports = __toCommonJS(keystone_exports); | ||||
| var import_core8 = require("@keystone-6/core"); | ||||
|  | ||||
| // schema/index.ts | ||||
| var import_core7 = require("@keystone-6/core"); | ||||
| var import_fields7 = require("@keystone-6/core/fields"); | ||||
|  | ||||
| // limit.ts | ||||
| var isUser = ({ session: session2 }) => session2?.data.id != null; | ||||
| var allowUser = { | ||||
|   operation: { | ||||
|     create: isUser, | ||||
|     update: isUser, | ||||
|     delete: isUser | ||||
|   } | ||||
| }; | ||||
| var isEditor = ({ session: session2 }) => session2?.data.isEditor || session2?.data.isAdmin; | ||||
| var allowEditor = { | ||||
|   operation: { | ||||
|     create: isEditor, | ||||
|     update: isEditor, | ||||
|     delete: isEditor | ||||
|   } | ||||
| }; | ||||
| var isAdmin = ({ session: session2 }) => session2?.data.isAdmin; | ||||
| var allowAdmin = { | ||||
|   operation: { | ||||
|     create: isAdmin, | ||||
|     update: isAdmin, | ||||
|     delete: isAdmin | ||||
|   } | ||||
| }; | ||||
|  | ||||
| // schema/assets.ts | ||||
| var import_fields = require("@keystone-6/core/fields"); | ||||
| var import_core = require("@keystone-6/core"); | ||||
| var Image = (0, import_core.list)({ | ||||
|   access: allowEditor, | ||||
|   fields: { | ||||
|     caption: (0, import_fields.text)(), | ||||
|     image: (0, import_fields.image)({ storage: "localImages" }), | ||||
|     createdAt: (0, import_fields.timestamp)({ | ||||
|       defaultValue: { kind: "now" } | ||||
|     }) | ||||
|   } | ||||
| }); | ||||
| var Asset = (0, import_core.list)({ | ||||
|   access: allowEditor, | ||||
|   fields: { | ||||
|     caption: (0, import_fields.text)(), | ||||
|     url: (0, import_fields.text)({ validation: { isRequired: true } }), | ||||
|     type: (0, import_fields.select)({ | ||||
|       type: "enum", | ||||
|       options: [ | ||||
|         { label: "Video", value: "video" }, | ||||
|         { label: "Audio", value: "audio" } | ||||
|       ], | ||||
|       defaultValue: "video", | ||||
|       db: { map: "media_type" }, | ||||
|       validation: { isRequired: true }, | ||||
|       ui: { displayMode: "select" } | ||||
|     }), | ||||
|     createdAt: (0, import_fields.timestamp)({ | ||||
|       defaultValue: { kind: "now" } | ||||
|     }) | ||||
|   } | ||||
| }); | ||||
|  | ||||
| // schema/moments.ts | ||||
| var import_core2 = require("@keystone-6/core"); | ||||
| var import_fields_document = require("@keystone-6/fields-document"); | ||||
| var import_fields2 = require("@keystone-6/core/fields"); | ||||
| var Moment = (0, import_core2.list)({ | ||||
|   access: allowUser, | ||||
|   fields: { | ||||
|     title: (0, import_fields2.text)({ validation: { isRequired: true } }), | ||||
|     images: (0, import_fields2.relationship)({ ref: "Image", many: true }), | ||||
|     content: (0, import_fields_document.document)({ | ||||
|       formatting: true, | ||||
|       layouts: [ | ||||
|         [1, 1], | ||||
|         [1, 1, 1], | ||||
|         [2, 1], | ||||
|         [1, 2], | ||||
|         [1, 2, 1] | ||||
|       ], | ||||
|       links: true, | ||||
|       dividers: true | ||||
|     }), | ||||
|     author: (0, import_fields2.relationship)({ | ||||
|       ref: "User.moments", | ||||
|       ui: { | ||||
|         displayMode: "cards", | ||||
|         cardFields: ["name", "email"], | ||||
|         inlineEdit: { fields: ["name", "email"] }, | ||||
|         linkToItem: true, | ||||
|         inlineConnect: true | ||||
|       }, | ||||
|       many: false | ||||
|     }), | ||||
|     categories: (0, import_fields2.relationship)({ | ||||
|       ref: "Category.moments", | ||||
|       many: true, | ||||
|       ui: { | ||||
|         displayMode: "cards", | ||||
|         cardFields: ["name"], | ||||
|         inlineEdit: { fields: ["name"] }, | ||||
|         linkToItem: true, | ||||
|         inlineConnect: true, | ||||
|         inlineCreate: { fields: ["name"] } | ||||
|       } | ||||
|     }), | ||||
|     tags: (0, import_fields2.relationship)({ | ||||
|       ref: "Tag.moments", | ||||
|       many: true, | ||||
|       ui: { | ||||
|         displayMode: "cards", | ||||
|         cardFields: ["name"], | ||||
|         inlineEdit: { fields: ["name"] }, | ||||
|         linkToItem: true, | ||||
|         inlineConnect: true, | ||||
|         inlineCreate: { fields: ["name"] } | ||||
|       } | ||||
|     }), | ||||
|     createdAt: (0, import_fields2.timestamp)({ | ||||
|       defaultValue: { kind: "now" } | ||||
|     }) | ||||
|   } | ||||
| }); | ||||
|  | ||||
| // schema/categories.ts | ||||
| var import_core3 = require("@keystone-6/core"); | ||||
| var import_fields3 = require("@keystone-6/core/fields"); | ||||
| var Category = (0, import_core3.list)({ | ||||
|   access: allowEditor, | ||||
|   fields: { | ||||
|     slug: (0, import_fields3.text)({ | ||||
|       validation: { | ||||
|         isRequired: true | ||||
|       }, | ||||
|       isIndexed: "unique" | ||||
|     }), | ||||
|     name: (0, import_fields3.text)(), | ||||
|     posts: (0, import_fields3.relationship)({ ref: "Post.categories", many: true }), | ||||
|     moments: (0, import_fields3.relationship)({ ref: "Moment.categories", many: true }), | ||||
|     events: (0, import_fields3.relationship)({ ref: "Event.categories", many: true }) | ||||
|   } | ||||
| }); | ||||
| var Tag = (0, import_core3.list)({ | ||||
|   access: allowEditor, | ||||
|   fields: { | ||||
|     slug: (0, import_fields3.text)({ | ||||
|       validation: { | ||||
|         isRequired: true | ||||
|       }, | ||||
|       isIndexed: "unique" | ||||
|     }), | ||||
|     name: (0, import_fields3.text)(), | ||||
|     posts: (0, import_fields3.relationship)({ ref: "Post.tags", many: true }), | ||||
|     moments: (0, import_fields3.relationship)({ ref: "Moment.tags", many: true }), | ||||
|     events: (0, import_fields3.relationship)({ ref: "Event.tags", many: true }) | ||||
|   } | ||||
| }); | ||||
|  | ||||
| // schema/projects.ts | ||||
| var import_fields4 = require("@keystone-6/core/fields"); | ||||
| var import_core4 = require("@keystone-6/core"); | ||||
| var Project = (0, import_core4.list)({ | ||||
|   access: { | ||||
|     ...allowAdmin, | ||||
|     filter: { | ||||
|       query: ({ session: session2 }) => { | ||||
|         if (session2?.data.isEditor || session2?.data.isAdmin) | ||||
|           return true; | ||||
|         return { isPublished: { equals: true } }; | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   fields: { | ||||
|     icon: (0, import_fields4.relationship)({ ref: "Image" }), | ||||
|     name: (0, import_fields4.text)({ validation: { isRequired: true } }), | ||||
|     description: (0, import_fields4.text)(), | ||||
|     link: (0, import_fields4.text)(), | ||||
|     isPublished: (0, import_fields4.checkbox)(), | ||||
|     status: (0, import_fields4.select)({ | ||||
|       type: "enum", | ||||
|       options: [ | ||||
|         { label: "Pending", value: "pending" }, | ||||
|         { label: "Constructing", value: "constructing" }, | ||||
|         { label: "Published", value: "published" }, | ||||
|         { label: "Abandoned", value: "abandoned" } | ||||
|       ], | ||||
|       defaultValue: "pending", | ||||
|       db: { map: "project_status" }, | ||||
|       validation: { isRequired: true }, | ||||
|       ui: { displayMode: "select" } | ||||
|     }), | ||||
|     post: (0, import_fields4.relationship)({ ref: "Post" }), | ||||
|     createdAt: (0, import_fields4.timestamp)({ | ||||
|       defaultValue: { kind: "now" } | ||||
|     }) | ||||
|   } | ||||
| }); | ||||
|  | ||||
| // schema/posts.ts | ||||
| var import_fields5 = require("@keystone-6/core/fields"); | ||||
| var import_fields_document2 = require("@keystone-6/fields-document"); | ||||
| var import_core5 = require("@keystone-6/core"); | ||||
| var Post = (0, import_core5.list)({ | ||||
|   access: { | ||||
|     ...allowEditor, | ||||
|     filter: { | ||||
|       query: ({ session: session2 }) => { | ||||
|         if (session2?.data.isEditor || session2?.data.isAdmin) | ||||
|           return true; | ||||
|         return { isPublished: { equals: true } }; | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   fields: { | ||||
|     slug: (0, import_fields5.text)({ | ||||
|       validation: { | ||||
|         isRequired: true | ||||
|       }, | ||||
|       isIndexed: "unique" | ||||
|     }), | ||||
|     title: (0, import_fields5.text)({ validation: { isRequired: true } }), | ||||
|     cover: (0, import_fields5.relationship)({ ref: "Image" }), | ||||
|     description: (0, import_fields5.text)(), | ||||
|     assets: (0, import_fields5.relationship)({ ref: "Asset", many: true }), | ||||
|     images: (0, import_fields5.relationship)({ ref: "Image", many: true }), | ||||
|     content: (0, import_fields_document2.document)({ | ||||
|       formatting: true, | ||||
|       layouts: [ | ||||
|         [1, 1], | ||||
|         [1, 1, 1], | ||||
|         [2, 1], | ||||
|         [1, 2], | ||||
|         [1, 2, 1] | ||||
|       ], | ||||
|       links: true, | ||||
|       dividers: true | ||||
|     }), | ||||
|     type: (0, import_fields5.select)({ | ||||
|       type: "enum", | ||||
|       options: [ | ||||
|         { label: "Article", value: "article" }, | ||||
|         { label: "Podcast", value: "podcast" } | ||||
|       ], | ||||
|       defaultValue: "article", | ||||
|       db: { map: "post_type" }, | ||||
|       validation: { isRequired: true }, | ||||
|       ui: { displayMode: "select" } | ||||
|     }), | ||||
|     isPublished: (0, import_fields5.checkbox)(), | ||||
|     author: (0, import_fields5.relationship)({ | ||||
|       ref: "User.posts", | ||||
|       ui: { | ||||
|         displayMode: "cards", | ||||
|         cardFields: ["name", "email"], | ||||
|         inlineEdit: { fields: ["name", "email"] }, | ||||
|         linkToItem: true, | ||||
|         inlineConnect: true | ||||
|       }, | ||||
|       many: false | ||||
|     }), | ||||
|     categories: (0, import_fields5.relationship)({ | ||||
|       ref: "Category.posts", | ||||
|       many: true, | ||||
|       ui: { | ||||
|         displayMode: "cards", | ||||
|         cardFields: ["name"], | ||||
|         inlineEdit: { fields: ["name"] }, | ||||
|         linkToItem: true, | ||||
|         inlineConnect: true, | ||||
|         inlineCreate: { fields: ["name"] } | ||||
|       } | ||||
|     }), | ||||
|     tags: (0, import_fields5.relationship)({ | ||||
|       ref: "Tag.posts", | ||||
|       many: true, | ||||
|       ui: { | ||||
|         displayMode: "cards", | ||||
|         cardFields: ["name"], | ||||
|         inlineEdit: { fields: ["name"] }, | ||||
|         linkToItem: true, | ||||
|         inlineConnect: true, | ||||
|         inlineCreate: { fields: ["name"] } | ||||
|       } | ||||
|     }), | ||||
|     createdAt: (0, import_fields5.timestamp)({ | ||||
|       defaultValue: { kind: "now" } | ||||
|     }) | ||||
|   } | ||||
| }); | ||||
|  | ||||
| // schema/events.ts | ||||
| var import_fields6 = require("@keystone-6/core/fields"); | ||||
| var import_fields_document3 = require("@keystone-6/fields-document"); | ||||
| var import_core6 = require("@keystone-6/core"); | ||||
| var Event = (0, import_core6.list)({ | ||||
|   access: { | ||||
|     ...allowEditor, | ||||
|     filter: { | ||||
|       query: ({ session: session2 }) => { | ||||
|         if (session2?.data.isEditor || session2?.data.isAdmin) | ||||
|           return true; | ||||
|         return { isPublished: { equals: true } }; | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   fields: { | ||||
|     slug: (0, import_fields6.text)({ | ||||
|       validation: { | ||||
|         isRequired: true | ||||
|       }, | ||||
|       isIndexed: "unique" | ||||
|     }), | ||||
|     title: (0, import_fields6.text)({ validation: { isRequired: true } }), | ||||
|     description: (0, import_fields6.text)(), | ||||
|     content: (0, import_fields_document3.document)({ | ||||
|       formatting: true, | ||||
|       layouts: [ | ||||
|         [1, 1], | ||||
|         [1, 1, 1], | ||||
|         [2, 1], | ||||
|         [1, 2], | ||||
|         [1, 2, 1] | ||||
|       ], | ||||
|       links: true, | ||||
|       dividers: true | ||||
|     }), | ||||
|     isPublished: (0, import_fields6.checkbox)(), | ||||
|     isHistory: (0, import_fields6.checkbox)(), | ||||
|     author: (0, import_fields6.relationship)({ | ||||
|       ref: "User.events", | ||||
|       ui: { | ||||
|         displayMode: "cards", | ||||
|         cardFields: ["name", "email"], | ||||
|         inlineEdit: { fields: ["name", "email"] }, | ||||
|         linkToItem: true, | ||||
|         inlineConnect: true | ||||
|       }, | ||||
|       many: false | ||||
|     }), | ||||
|     categories: (0, import_fields6.relationship)({ | ||||
|       ref: "Category.events", | ||||
|       many: true, | ||||
|       ui: { | ||||
|         displayMode: "cards", | ||||
|         cardFields: ["name"], | ||||
|         inlineEdit: { fields: ["name"] }, | ||||
|         linkToItem: true, | ||||
|         inlineConnect: true, | ||||
|         inlineCreate: { fields: ["name"] } | ||||
|       } | ||||
|     }), | ||||
|     tags: (0, import_fields6.relationship)({ | ||||
|       ref: "Tag.events", | ||||
|       many: true, | ||||
|       ui: { | ||||
|         displayMode: "cards", | ||||
|         cardFields: ["name"], | ||||
|         inlineEdit: { fields: ["name"] }, | ||||
|         linkToItem: true, | ||||
|         inlineConnect: true, | ||||
|         inlineCreate: { fields: ["name"] } | ||||
|       } | ||||
|     }), | ||||
|     createdAt: (0, import_fields6.timestamp)({ | ||||
|       defaultValue: { kind: "now" } | ||||
|     }) | ||||
|   } | ||||
| }); | ||||
|  | ||||
| // schema/index.ts | ||||
| var lists = { | ||||
|   User: (0, import_core7.list)({ | ||||
|     access: allowAdmin, | ||||
|     fields: { | ||||
|       name: (0, import_fields7.text)({ validation: { isRequired: true } }), | ||||
|       email: (0, import_fields7.text)({ | ||||
|         validation: { isRequired: true }, | ||||
|         isIndexed: "unique" | ||||
|       }), | ||||
|       password: (0, import_fields7.password)({ validation: { isRequired: true } }), | ||||
|       posts: (0, import_fields7.relationship)({ ref: "Post.author", many: true }), | ||||
|       moments: (0, import_fields7.relationship)({ ref: "Moment.author", many: true }), | ||||
|       events: (0, import_fields7.relationship)({ ref: "Event.author", many: true }), | ||||
|       isAdmin: (0, import_fields7.checkbox)(), | ||||
|       isEditor: (0, import_fields7.checkbox)(), | ||||
|       createdAt: (0, import_fields7.timestamp)({ | ||||
|         defaultValue: { kind: "now" } | ||||
|       }) | ||||
|     } | ||||
|   }), | ||||
|   Image, | ||||
|   Asset, | ||||
|   Post, | ||||
|   Moment, | ||||
|   Project, | ||||
|   Event, | ||||
|   Category, | ||||
|   Tag | ||||
| }; | ||||
|  | ||||
| // auth.ts | ||||
| var import_crypto = require("crypto"); | ||||
| var import_auth = require("@keystone-6/auth"); | ||||
| var import_session = require("@keystone-6/core/session"); | ||||
| var sessionSecret = process.env.SESSION_SECRET; | ||||
| if (!sessionSecret && process.env.NODE_ENV !== "production") { | ||||
|   sessionSecret = (0, import_crypto.randomBytes)(32).toString("hex"); | ||||
| } | ||||
| var { withAuth } = (0, import_auth.createAuth)({ | ||||
|   listKey: "User", | ||||
|   identityField: "email", | ||||
|   sessionData: "id name createdAt isAdmin isEditor", | ||||
|   secretField: "password", | ||||
|   initFirstItem: { | ||||
|     fields: ["name", "email", "password", "isAdmin"] | ||||
|   } | ||||
| }); | ||||
| var sessionMaxAge = 60 * 60 * 24 * 30; | ||||
| var session = (0, import_session.statelessSessions)({ | ||||
|   maxAge: sessionMaxAge, | ||||
|   secret: sessionSecret | ||||
| }); | ||||
|  | ||||
| // keystone.ts | ||||
| var baseUrl = process.env.BASE_URL ?? "http://localhost:3000"; | ||||
| var databaseUrl = process.env.DATABASE_URL ?? "postgresql://postgres:password@127.0.0.1:5432/capital"; | ||||
| var databaseProvider = process.env.DATABASE_PROVIDER ?? "postgresql"; | ||||
| var keystone_default = withAuth( | ||||
|   (0, import_core8.config)({ | ||||
|     ui: { | ||||
|       basePath: "/cms" | ||||
|     }, | ||||
|     db: { | ||||
|       provider: databaseProvider, | ||||
|       url: databaseUrl | ||||
|     }, | ||||
|     server: { | ||||
|       cors: { | ||||
|         origin: "*", | ||||
|         methods: "GET,HEAD,PUT,PATCH,POST,DELETE" | ||||
|       } | ||||
|     }, | ||||
|     storage: { | ||||
|       localImages: { | ||||
|         kind: "local", | ||||
|         type: "image", | ||||
|         generateUrl: (path) => `${baseUrl}/images${path}`, | ||||
|         serverRoute: { | ||||
|           path: "/images" | ||||
|         }, | ||||
|         storagePath: "public/images" | ||||
|       } | ||||
|     }, | ||||
|     lists, | ||||
|     session | ||||
|   }) | ||||
| ); | ||||
| //# sourceMappingURL=config.js.map | ||||
							
								
								
									
										7
									
								
								content/.keystone/config.js.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								content/.keystone/config.js.map
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										40
									
								
								content/auth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								content/auth.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| import { randomBytes } from "crypto"; | ||||
| import { createAuth } from "@keystone-6/auth"; | ||||
|  | ||||
| import { statelessSessions } from "@keystone-6/core/session"; | ||||
|  | ||||
| let sessionSecret = process.env.SESSION_SECRET; | ||||
| if (!sessionSecret && process.env.NODE_ENV !== "production") { | ||||
|   sessionSecret = randomBytes(32).toString("hex"); | ||||
| } | ||||
|  | ||||
| export type Session = { | ||||
|   data: { | ||||
|     id: string; | ||||
|     name: string; | ||||
|     isAdmin: boolean; | ||||
|     isEditor: boolean; | ||||
|     createdAt: Date; | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| const { withAuth } = createAuth({ | ||||
|   listKey: "User", | ||||
|   identityField: "email", | ||||
|  | ||||
|   sessionData: "id name createdAt isAdmin isEditor", | ||||
|   secretField: "password", | ||||
|  | ||||
|   initFirstItem: { | ||||
|     fields: ["name", "email", "password", "isAdmin"], | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| const sessionMaxAge = 60 * 60 * 24 * 30; | ||||
|  | ||||
| const session = statelessSessions({ | ||||
|   maxAge: sessionMaxAge, | ||||
|   secret: sessionSecret!, | ||||
| }); | ||||
|  | ||||
| export { withAuth, session }; | ||||
							
								
								
									
										6
									
								
								content/entrypoint.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								content/entrypoint.sh
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| #!/bin/sh | ||||
|  | ||||
| cd /app; | ||||
| npm install; | ||||
| npx prisma db push; | ||||
| npm run start; | ||||
							
								
								
									
										43
									
								
								content/keystone.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								content/keystone.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| import { config } from "@keystone-6/core"; | ||||
|  | ||||
| import { lists } from "./schema"; | ||||
|  | ||||
| import { withAuth, session } from "./auth"; | ||||
| import { DatabaseProvider } from "@keystone-6/core/types"; | ||||
|  | ||||
| const baseUrl = process.env.BASE_URL ?? "http://localhost:3000"; | ||||
| const databaseUrl = | ||||
|   process.env.DATABASE_URL ?? | ||||
|   "postgresql://postgres:password@127.0.0.1:5432/capital"; | ||||
| const databaseProvider = process.env.DATABASE_PROVIDER ?? "postgresql"; | ||||
|  | ||||
| export default withAuth( | ||||
|   config({ | ||||
|     ui: { | ||||
|       basePath: "/cms" | ||||
|     }, | ||||
|     db: { | ||||
|       provider: databaseProvider as DatabaseProvider, | ||||
|       url: databaseUrl, | ||||
|     }, | ||||
|     server: { | ||||
|       cors: { | ||||
|         origin: "*", | ||||
|         methods: "GET,HEAD,PUT,PATCH,POST,DELETE", | ||||
|       }, | ||||
|     }, | ||||
|     storage: { | ||||
|       localImages: { | ||||
|         kind: "local", | ||||
|         type: "image", | ||||
|         generateUrl: (path) => `${baseUrl}/images${path}`, | ||||
|         serverRoute: { | ||||
|           path: "/images", | ||||
|         }, | ||||
|         storagePath: "public/images", | ||||
|       }, | ||||
|     }, | ||||
|     lists, | ||||
|     session, | ||||
|   }) | ||||
| ); | ||||
							
								
								
									
										29
									
								
								content/limit.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								content/limit.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| const isUser = ({ session }: { session: any }) => session?.data.id != null; | ||||
| const allowUser: any = { | ||||
|   operation: { | ||||
|     create: isUser, | ||||
|     update: isUser, | ||||
|     delete: isUser, | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| const isEditor = ({ session }: { session: any }) => | ||||
|   session?.data.isEditor || session?.data.isAdmin; | ||||
| const allowEditor: any = { | ||||
|   operation: { | ||||
|     create: isEditor, | ||||
|     update: isEditor, | ||||
|     delete: isEditor, | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| const isAdmin = ({ session }: { session: any }) => session?.data.isAdmin; | ||||
| const allowAdmin: any = { | ||||
|   operation: { | ||||
|     create: isAdmin, | ||||
|     update: isAdmin, | ||||
|     delete: isAdmin, | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export { isUser, isAdmin, isEditor, allowUser, allowAdmin, allowEditor }; | ||||
							
								
								
									
										11019
									
								
								content/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										11019
									
								
								content/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										17
									
								
								content/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								content/package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| { | ||||
|   "name": "keystone-app", | ||||
|   "version": "1.0.2", | ||||
|   "private": true, | ||||
|   "scripts": { | ||||
|     "dev": "keystone dev", | ||||
|     "start": "keystone start", | ||||
|     "build": "keystone build", | ||||
|     "postinstall": "keystone build --no-ui --frozen" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@keystone-6/auth": "^7.0.0", | ||||
|     "@keystone-6/core": "^5.0.0", | ||||
|     "@keystone-6/fields-document": "^7.0.0", | ||||
|     "typescript": "^4.9.5" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								content/public/images/503190c9-4199-4290-85cb-714d725b3483.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								content/public/images/503190c9-4199-4290-85cb-714d725b3483.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.3 MiB | 
							
								
								
									
										
											BIN
										
									
								
								content/public/images/c40ae4e2-9e5d-48bb-918c-2a98d25d4266.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								content/public/images/c40ae4e2-9e5d-48bb-918c-2a98d25d4266.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.3 MiB | 
							
								
								
									
										1104
									
								
								content/schema.graphql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1104
									
								
								content/schema.graphql
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										155
									
								
								content/schema.prisma
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								content/schema.prisma
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | ||||
| // This file is automatically generated by Keystone, do not modify it manually. | ||||
| // Modify your Keystone config when you want to change this. | ||||
|  | ||||
| datasource postgresql { | ||||
|   url               = env("DATABASE_URL") | ||||
|   shadowDatabaseUrl = env("SHADOW_DATABASE_URL") | ||||
|   provider          = "postgresql" | ||||
| } | ||||
|  | ||||
| generator client { | ||||
|   provider = "prisma-client-js" | ||||
| } | ||||
|  | ||||
| model User { | ||||
|   id        String    @id @default(cuid()) | ||||
|   name      String    @default("") | ||||
|   email     String    @unique @default("") | ||||
|   password  String | ||||
|   posts     Post[]    @relation("Post_author") | ||||
|   moments   Moment[]  @relation("Moment_author") | ||||
|   events    Event[]   @relation("Event_author") | ||||
|   isAdmin   Boolean   @default(false) | ||||
|   isEditor  Boolean   @default(false) | ||||
|   createdAt DateTime? @default(now()) | ||||
| } | ||||
|  | ||||
| model Image { | ||||
|   id                 String    @id @default(cuid()) | ||||
|   caption            String    @default("") | ||||
|   image_filesize     Int? | ||||
|   image_extension    String? | ||||
|   image_width        Int? | ||||
|   image_height       Int? | ||||
|   image_id           String? | ||||
|   createdAt          DateTime? @default(now()) | ||||
|   from_Post_cover    Post[]    @relation("Post_cover") | ||||
|   from_Post_images   Post[]    @relation("Post_images") | ||||
|   from_Moment_images Moment[]  @relation("Moment_images") | ||||
|   from_Project_icon  Project[] @relation("Project_icon") | ||||
| } | ||||
|  | ||||
| model Asset { | ||||
|   id               String        @id @default(cuid()) | ||||
|   caption          String        @default("") | ||||
|   url              String        @default("") | ||||
|   type             AssetTypeType @default(video) @map("media_type") | ||||
|   createdAt        DateTime?     @default(now()) | ||||
|   from_Post_assets Post[]        @relation("Post_assets") | ||||
| } | ||||
|  | ||||
| model Post { | ||||
|   id                String       @id @default(cuid()) | ||||
|   slug              String       @unique @default("") | ||||
|   title             String       @default("") | ||||
|   cover             Image?       @relation("Post_cover", fields: [coverId], references: [id]) | ||||
|   coverId           String?      @map("cover") | ||||
|   description       String       @default("") | ||||
|   assets            Asset[]      @relation("Post_assets") | ||||
|   images            Image[]      @relation("Post_images") | ||||
|   content           Json         @default("[{\"type\":\"paragraph\",\"children\":[{\"text\":\"\"}]}]") | ||||
|   type              PostTypeType @default(article) @map("post_type") | ||||
|   isPublished       Boolean      @default(false) | ||||
|   author            User?        @relation("Post_author", fields: [authorId], references: [id]) | ||||
|   authorId          String?      @map("author") | ||||
|   categories        Category[]   @relation("Category_posts") | ||||
|   tags              Tag[]        @relation("Post_tags") | ||||
|   createdAt         DateTime?    @default(now()) | ||||
|   from_Project_post Project[]    @relation("Project_post") | ||||
|  | ||||
|   @@index([coverId]) | ||||
|   @@index([authorId]) | ||||
| } | ||||
|  | ||||
| model Moment { | ||||
|   id         String     @id @default(cuid()) | ||||
|   title      String     @default("") | ||||
|   images     Image[]    @relation("Moment_images") | ||||
|   content    Json       @default("[{\"type\":\"paragraph\",\"children\":[{\"text\":\"\"}]}]") | ||||
|   author     User?      @relation("Moment_author", fields: [authorId], references: [id]) | ||||
|   authorId   String?    @map("author") | ||||
|   categories Category[] @relation("Category_moments") | ||||
|   tags       Tag[]      @relation("Moment_tags") | ||||
|   createdAt  DateTime?  @default(now()) | ||||
|  | ||||
|   @@index([authorId]) | ||||
| } | ||||
|  | ||||
| model Project { | ||||
|   id          String            @id @default(cuid()) | ||||
|   icon        Image?            @relation("Project_icon", fields: [iconId], references: [id]) | ||||
|   iconId      String?           @map("icon") | ||||
|   name        String            @default("") | ||||
|   description String            @default("") | ||||
|   link        String            @default("") | ||||
|   isPublished Boolean           @default(false) | ||||
|   status      ProjectStatusType @default(pending) @map("project_status") | ||||
|   post        Post?             @relation("Project_post", fields: [postId], references: [id]) | ||||
|   postId      String?           @map("post") | ||||
|   createdAt   DateTime?         @default(now()) | ||||
|  | ||||
|   @@index([iconId]) | ||||
|   @@index([postId]) | ||||
| } | ||||
|  | ||||
| model Event { | ||||
|   id          String     @id @default(cuid()) | ||||
|   slug        String     @unique @default("") | ||||
|   title       String     @default("") | ||||
|   description String     @default("") | ||||
|   content     Json       @default("[{\"type\":\"paragraph\",\"children\":[{\"text\":\"\"}]}]") | ||||
|   isPublished Boolean    @default(false) | ||||
|   isHistory   Boolean    @default(false) | ||||
|   author      User?      @relation("Event_author", fields: [authorId], references: [id]) | ||||
|   authorId    String?    @map("author") | ||||
|   categories  Category[] @relation("Category_events") | ||||
|   tags        Tag[]      @relation("Event_tags") | ||||
|   createdAt   DateTime?  @default(now()) | ||||
|  | ||||
|   @@index([authorId]) | ||||
| } | ||||
|  | ||||
| model Category { | ||||
|   id      String   @id @default(cuid()) | ||||
|   slug    String   @unique @default("") | ||||
|   name    String   @default("") | ||||
|   posts   Post[]   @relation("Category_posts") | ||||
|   moments Moment[] @relation("Category_moments") | ||||
|   events  Event[]  @relation("Category_events") | ||||
| } | ||||
|  | ||||
| model Tag { | ||||
|   id      String   @id @default(cuid()) | ||||
|   slug    String   @unique @default("") | ||||
|   name    String   @default("") | ||||
|   posts   Post[]   @relation("Post_tags") | ||||
|   moments Moment[] @relation("Moment_tags") | ||||
|   events  Event[]  @relation("Event_tags") | ||||
| } | ||||
|  | ||||
| enum AssetTypeType { | ||||
|   video | ||||
|   audio | ||||
| } | ||||
|  | ||||
| enum PostTypeType { | ||||
|   article | ||||
|   podcast | ||||
| } | ||||
|  | ||||
| enum ProjectStatusType { | ||||
|   pending | ||||
|   constructing | ||||
|   published | ||||
|   abandoned | ||||
| } | ||||
							
								
								
									
										41
									
								
								content/schema/assets.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								content/schema/assets.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| import { image, select, text, timestamp } from "@keystone-6/core/fields"; | ||||
| import { list } from "@keystone-6/core"; | ||||
|  | ||||
| import { allowEditor } from "../limit"; | ||||
|  | ||||
| export const Image = list({ | ||||
|   access: allowEditor, | ||||
|  | ||||
|   fields: { | ||||
|     caption: text(), | ||||
|     image: image({ storage: "localImages" }), | ||||
|  | ||||
|     createdAt: timestamp({ | ||||
|       defaultValue: { kind: "now" }, | ||||
|     }), | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| export const Asset = list({ | ||||
|   access: allowEditor, | ||||
|  | ||||
|   fields: { | ||||
|     caption: text(), | ||||
|     url: text({ validation: { isRequired: true } }), | ||||
|     type: select({ | ||||
|       type: "enum", | ||||
|       options: [ | ||||
|         { label: "Video", value: "video" }, | ||||
|         { label: "Audio", value: "audio" }, | ||||
|       ], | ||||
|       defaultValue: "video", | ||||
|       db: { map: "media_type" }, | ||||
|       validation: { isRequired: true }, | ||||
|       ui: { displayMode: "select" }, | ||||
|     }), | ||||
|  | ||||
|     createdAt: timestamp({ | ||||
|       defaultValue: { kind: "now" }, | ||||
|     }), | ||||
|   }, | ||||
| }); | ||||
							
								
								
									
										37
									
								
								content/schema/categories.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								content/schema/categories.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| import { list } from "@keystone-6/core"; | ||||
| import { allowEditor } from "../limit"; | ||||
| import { relationship, text } from "@keystone-6/core/fields"; | ||||
|  | ||||
| export const Category = list({ | ||||
|   access: allowEditor, | ||||
|  | ||||
|   fields: { | ||||
|     slug: text({ | ||||
|       validation: { | ||||
|         isRequired: true, | ||||
|       }, | ||||
|       isIndexed: "unique", | ||||
|     }), | ||||
|     name: text(), | ||||
|     posts: relationship({ ref: "Post.categories", many: true }), | ||||
|     moments: relationship({ ref: "Moment.categories", many: true }), | ||||
|     events: relationship({ ref: "Event.categories", many: true }), | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| export const Tag = list({ | ||||
|   access: allowEditor, | ||||
|  | ||||
|   fields: { | ||||
|     slug: text({ | ||||
|       validation: { | ||||
|         isRequired: true, | ||||
|       }, | ||||
|       isIndexed: "unique", | ||||
|     }), | ||||
|     name: text(), | ||||
|     posts: relationship({ ref: "Post.tags", many: true }), | ||||
|     moments: relationship({ ref: "Moment.tags", many: true }), | ||||
|     events: relationship({ ref: "Event.tags", many: true }), | ||||
|   }, | ||||
| }); | ||||
							
								
								
									
										94
									
								
								content/schema/events.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								content/schema/events.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| import { | ||||
|   checkbox, | ||||
|   relationship, | ||||
|   text, | ||||
|   timestamp, | ||||
| } from "@keystone-6/core/fields"; | ||||
| import { document } from "@keystone-6/fields-document"; | ||||
| import { list } from "@keystone-6/core"; | ||||
| import { allowEditor } from "../limit"; | ||||
| import { Session } from "../auth"; | ||||
|  | ||||
| export const Event = list({ | ||||
|   access: { | ||||
|     ...allowEditor, | ||||
|  | ||||
|     filter: { | ||||
|       query: ({ session }: { session: Session }) => { | ||||
|         if (session?.data.isEditor || session?.data.isAdmin) return true; | ||||
|         return { isPublished: { equals: true } }; | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
|   fields: { | ||||
|     slug: text({ | ||||
|       validation: { | ||||
|         isRequired: true, | ||||
|       }, | ||||
|       isIndexed: "unique", | ||||
|     }), | ||||
|     title: text({ validation: { isRequired: true } }), | ||||
|     description: text(), | ||||
|  | ||||
|     content: document({ | ||||
|       formatting: true, | ||||
|       layouts: [ | ||||
|         [1, 1], | ||||
|         [1, 1, 1], | ||||
|         [2, 1], | ||||
|         [1, 2], | ||||
|         [1, 2, 1], | ||||
|       ], | ||||
|       links: true, | ||||
|       dividers: true, | ||||
|     }), | ||||
|  | ||||
|     isPublished: checkbox(), | ||||
|     isHistory: checkbox(), | ||||
|  | ||||
|     author: relationship({ | ||||
|       ref: "User.events", | ||||
|  | ||||
|       ui: { | ||||
|         displayMode: "cards", | ||||
|         cardFields: ["name", "email"], | ||||
|         inlineEdit: { fields: ["name", "email"] }, | ||||
|         linkToItem: true, | ||||
|         inlineConnect: true, | ||||
|       }, | ||||
|  | ||||
|       many: false, | ||||
|     }), | ||||
|  | ||||
|     categories: relationship({ | ||||
|       ref: "Category.events", | ||||
|       many: true, | ||||
|       ui: { | ||||
|         displayMode: "cards", | ||||
|         cardFields: ["name"], | ||||
|         inlineEdit: { fields: ["name"] }, | ||||
|         linkToItem: true, | ||||
|         inlineConnect: true, | ||||
|         inlineCreate: { fields: ["name"] }, | ||||
|       }, | ||||
|     }), | ||||
|  | ||||
|     tags: relationship({ | ||||
|       ref: "Tag.events", | ||||
|       many: true, | ||||
|       ui: { | ||||
|         displayMode: "cards", | ||||
|         cardFields: ["name"], | ||||
|         inlineEdit: { fields: ["name"] }, | ||||
|         linkToItem: true, | ||||
|         inlineConnect: true, | ||||
|         inlineCreate: { fields: ["name"] }, | ||||
|       }, | ||||
|     }), | ||||
|  | ||||
|     createdAt: timestamp({ | ||||
|       defaultValue: { kind: "now" }, | ||||
|     }), | ||||
|   }, | ||||
| }); | ||||
							
								
								
									
										55
									
								
								content/schema/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								content/schema/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| import { list } from "@keystone-6/core"; | ||||
|  | ||||
| import { | ||||
|   text, | ||||
|   relationship, | ||||
|   password, | ||||
|   timestamp, | ||||
|   checkbox, | ||||
| } from "@keystone-6/core/fields"; | ||||
|  | ||||
| import { allowAdmin } from "../limit"; | ||||
|  | ||||
| import { Image, Asset } from "./assets"; | ||||
| import { Moment } from "./moments"; | ||||
| import { Category, Tag } from "./categories"; | ||||
| import { Project } from "./projects"; | ||||
| import { Post } from "./posts"; | ||||
| import { Event } from "./events"; | ||||
|  | ||||
| export const lists = { | ||||
|   User: list({ | ||||
|     access: allowAdmin, | ||||
|  | ||||
|     fields: { | ||||
|       name: text({ validation: { isRequired: true } }), | ||||
|       email: text({ | ||||
|         validation: { isRequired: true }, | ||||
|         isIndexed: "unique", | ||||
|       }), | ||||
|  | ||||
|       password: password({ validation: { isRequired: true } }), | ||||
|       posts: relationship({ ref: "Post.author", many: true }), | ||||
|       moments: relationship({ ref: "Moment.author", many: true }), | ||||
|       events: relationship({ ref: "Event.author", many: true }), | ||||
|  | ||||
|       isAdmin: checkbox(), | ||||
|       isEditor: checkbox(), | ||||
|  | ||||
|       createdAt: timestamp({ | ||||
|         defaultValue: { kind: "now" }, | ||||
|       }), | ||||
|     }, | ||||
|   }), | ||||
|  | ||||
|   Image, | ||||
|   Asset, | ||||
|  | ||||
|   Post, | ||||
|   Moment, | ||||
|   Project, | ||||
|   Event, | ||||
|  | ||||
|   Category, | ||||
|   Tag, | ||||
| }; | ||||
							
								
								
									
										70
									
								
								content/schema/moments.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								content/schema/moments.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| import { list } from "@keystone-6/core"; | ||||
| import { allowUser } from "../limit"; | ||||
| import { document } from "@keystone-6/fields-document"; | ||||
| import { relationship, text, timestamp } from "@keystone-6/core/fields"; | ||||
|  | ||||
| export const Moment = list({ | ||||
|     access: allowUser, | ||||
|  | ||||
|     fields: { | ||||
|       title: text({ validation: { isRequired: true } }), | ||||
|       images: relationship({ ref: "Image", many: true }), | ||||
|  | ||||
|       content: document({ | ||||
|         formatting: true, | ||||
|         layouts: [ | ||||
|           [1, 1], | ||||
|           [1, 1, 1], | ||||
|           [2, 1], | ||||
|           [1, 2], | ||||
|           [1, 2, 1], | ||||
|         ], | ||||
|         links: true, | ||||
|         dividers: true, | ||||
|       }), | ||||
|  | ||||
|       author: relationship({ | ||||
|         ref: "User.moments", | ||||
|  | ||||
|         ui: { | ||||
|           displayMode: "cards", | ||||
|           cardFields: ["name", "email"], | ||||
|           inlineEdit: { fields: ["name", "email"] }, | ||||
|           linkToItem: true, | ||||
|           inlineConnect: true, | ||||
|         }, | ||||
|  | ||||
|         many: false, | ||||
|       }), | ||||
|  | ||||
|       categories: relationship({ | ||||
|         ref: "Category.moments", | ||||
|         many: true, | ||||
|         ui: { | ||||
|           displayMode: "cards", | ||||
|           cardFields: ["name"], | ||||
|           inlineEdit: { fields: ["name"] }, | ||||
|           linkToItem: true, | ||||
|           inlineConnect: true, | ||||
|           inlineCreate: { fields: ["name"] }, | ||||
|         }, | ||||
|       }), | ||||
|  | ||||
|       tags: relationship({ | ||||
|         ref: "Tag.moments", | ||||
|         many: true, | ||||
|         ui: { | ||||
|           displayMode: "cards", | ||||
|           cardFields: ["name"], | ||||
|           inlineEdit: { fields: ["name"] }, | ||||
|           linkToItem: true, | ||||
|           inlineConnect: true, | ||||
|           inlineCreate: { fields: ["name"] }, | ||||
|         }, | ||||
|       }), | ||||
|  | ||||
|       createdAt: timestamp({ | ||||
|         defaultValue: { kind: "now" }, | ||||
|       }), | ||||
|     }, | ||||
|   }) | ||||
							
								
								
									
										110
									
								
								content/schema/posts.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								content/schema/posts.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| import { | ||||
|   checkbox, | ||||
|   relationship, | ||||
|   select, | ||||
|   text, | ||||
|   timestamp, | ||||
| } from "@keystone-6/core/fields"; | ||||
| import { document } from "@keystone-6/fields-document"; | ||||
| import { list } from "@keystone-6/core"; | ||||
| import { allowEditor } from "../limit"; | ||||
| import { Session } from "../auth"; | ||||
|  | ||||
| export const Post = list({ | ||||
|   access: { | ||||
|     ...allowEditor, | ||||
|  | ||||
|     filter: { | ||||
|       query: ({ session }: { session: Session }) => { | ||||
|         if (session?.data.isEditor || session?.data.isAdmin) return true; | ||||
|         return { isPublished: { equals: true } }; | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
|   fields: { | ||||
|     slug: text({ | ||||
|       validation: { | ||||
|         isRequired: true, | ||||
|       }, | ||||
|       isIndexed: "unique", | ||||
|     }), | ||||
|     title: text({ validation: { isRequired: true } }), | ||||
|     cover: relationship({ ref: "Image" }), | ||||
|  | ||||
|     description: text(), | ||||
|  | ||||
|     assets: relationship({ ref: "Asset", many: true }), | ||||
|     images: relationship({ ref: "Image", many: true }), | ||||
|     content: document({ | ||||
|       formatting: true, | ||||
|       layouts: [ | ||||
|         [1, 1], | ||||
|         [1, 1, 1], | ||||
|         [2, 1], | ||||
|         [1, 2], | ||||
|         [1, 2, 1], | ||||
|       ], | ||||
|       links: true, | ||||
|       dividers: true, | ||||
|     }), | ||||
|  | ||||
|     type: select({ | ||||
|       type: "enum", | ||||
|       options: [ | ||||
|         { label: "Article", value: "article" }, | ||||
|         { label: "Podcast", value: "podcast" }, | ||||
|       ], | ||||
|       defaultValue: "article", | ||||
|       db: { map: "post_type" }, | ||||
|       validation: { isRequired: true }, | ||||
|       ui: { displayMode: "select" }, | ||||
|     }), | ||||
|  | ||||
|     isPublished: checkbox(), | ||||
|  | ||||
|     author: relationship({ | ||||
|       ref: "User.posts", | ||||
|  | ||||
|       ui: { | ||||
|         displayMode: "cards", | ||||
|         cardFields: ["name", "email"], | ||||
|         inlineEdit: { fields: ["name", "email"] }, | ||||
|         linkToItem: true, | ||||
|         inlineConnect: true, | ||||
|       }, | ||||
|  | ||||
|       many: false, | ||||
|     }), | ||||
|  | ||||
|     categories: relationship({ | ||||
|       ref: "Category.posts", | ||||
|       many: true, | ||||
|       ui: { | ||||
|         displayMode: "cards", | ||||
|         cardFields: ["name"], | ||||
|         inlineEdit: { fields: ["name"] }, | ||||
|         linkToItem: true, | ||||
|         inlineConnect: true, | ||||
|         inlineCreate: { fields: ["name"] }, | ||||
|       }, | ||||
|     }), | ||||
|  | ||||
|     tags: relationship({ | ||||
|       ref: "Tag.posts", | ||||
|       many: true, | ||||
|       ui: { | ||||
|         displayMode: "cards", | ||||
|         cardFields: ["name"], | ||||
|         inlineEdit: { fields: ["name"] }, | ||||
|         linkToItem: true, | ||||
|         inlineConnect: true, | ||||
|         inlineCreate: { fields: ["name"] }, | ||||
|       }, | ||||
|     }), | ||||
|  | ||||
|     createdAt: timestamp({ | ||||
|       defaultValue: { kind: "now" }, | ||||
|     }), | ||||
|   }, | ||||
| }); | ||||
							
								
								
									
										46
									
								
								content/schema/projects.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								content/schema/projects.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| import { checkbox, relationship, select, text, timestamp } from "@keystone-6/core/fields"; | ||||
| import { list } from "@keystone-6/core"; | ||||
| import { allowAdmin } from "../limit"; | ||||
| import { Session } from "../auth"; | ||||
|  | ||||
| export const Project = list({ | ||||
|   access: { | ||||
|     ...allowAdmin, | ||||
|  | ||||
|     filter: { | ||||
|       query: ({ session }: { session: Session }) => { | ||||
|         if (session?.data.isEditor || session?.data.isAdmin) return true; | ||||
|         return { isPublished: { equals: true } }; | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
|   fields: { | ||||
|     icon: relationship({ ref: "Image" }), | ||||
|     name: text({ validation: { isRequired: true } }), | ||||
|     description: text(), | ||||
|     link: text(), | ||||
|  | ||||
|     isPublished: checkbox(), | ||||
|  | ||||
|     status: select({ | ||||
|       type: "enum", | ||||
|       options: [ | ||||
|         { label: "Pending", value: "pending" }, | ||||
|         { label: "Constructing", value: "constructing" }, | ||||
|         { label: "Published", value: "published" }, | ||||
|         { label: "Abandoned", value: "abandoned" }, | ||||
|       ], | ||||
|       defaultValue: "pending", | ||||
|       db: { map: "project_status" }, | ||||
|       validation: { isRequired: true }, | ||||
|       ui: { displayMode: "select" }, | ||||
|     }), | ||||
|  | ||||
|     post: relationship({ ref: "Post" }), | ||||
|  | ||||
|     createdAt: timestamp({ | ||||
|       defaultValue: { kind: "now" }, | ||||
|     }), | ||||
|   }, | ||||
| }); | ||||
							
								
								
									
										10
									
								
								content/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								content/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| { | ||||
|   "compilerOptions": { | ||||
|     "target": "esnext", | ||||
|     "module": "commonjs", | ||||
|     "strict": true, | ||||
|     "noEmit": true, | ||||
|     "esModuleInterop": true, | ||||
|     "forceConsistentCasingInFileNames": true, | ||||
|   } | ||||
| } | ||||
| @@ -3,7 +3,7 @@ | ||||
|   "type": "module", | ||||
|   "version": "0.0.1", | ||||
|   "scripts": { | ||||
|     "dev": "tinacms dev -c \"astro dev\"", | ||||
|     "dev": "astro dev", | ||||
|     "start": "astro dev", | ||||
|     "build": "astro check && astro build", | ||||
|     "preview": "astro preview", | ||||
| @@ -16,6 +16,7 @@ | ||||
|     "@astrojs/react": "^3.0.9", | ||||
|     "@astrojs/sitemap": "^3.0.5", | ||||
|     "@astrojs/tailwind": "^5.1.0", | ||||
|     "@keystone-6/document-renderer": "^1.1.2", | ||||
|     "@popperjs/core": "^2.11.8", | ||||
|     "@types/react": "^18.2.48", | ||||
|     "@types/react-dom": "^18.2.18", | ||||
| @@ -24,12 +25,10 @@ | ||||
|     "react-dom": "^18.2.0", | ||||
|     "sass": "^1.70.0", | ||||
|     "tailwindcss": "^3.4.1", | ||||
|     "tinacms": "^1.5.28", | ||||
|     "typescript": "^5.3.3" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@tailwindcss/typography": "^0.5.10", | ||||
|     "@tinacms/cli": "^1.5.39", | ||||
|     "@types/node": "^20.11.5", | ||||
|     "daisyui": "^4.6.0", | ||||
|     "prettier": "^3.2.4" | ||||
|   | ||||
| @@ -7,7 +7,7 @@ interface MenuItem { | ||||
|  | ||||
| const items: MenuItem[] = [ | ||||
|   { href: "/posts", label: "记录" }, | ||||
|   { href: "/events", label: "情报" }, | ||||
|   { href: "/events", label: "活动" }, | ||||
|   { href: "/projects", label: "企划" }, | ||||
| ]; | ||||
| --- | ||||
|   | ||||
| @@ -13,11 +13,11 @@ const { posts } = Astro.props; | ||||
|     posts?.map((item) => ( | ||||
|       <a href={`/posts/${item.slug}`}> | ||||
|         <div class="card sm:card-side hover:bg-base-200 transition-colors sm:max-w-none shadow-xl"> | ||||
|           {item.heroImg && ( | ||||
|           {item.cover.image.url && ( | ||||
|             <figure class="mx-auto w-full object-cover p-6 max-sm:pb-0 sm:max-w-[12rem] sm:pe-0"> | ||||
|               <img | ||||
|                 loading="lazy" | ||||
|                 src={item.heroImg} | ||||
|                 src={item.cover.image.url} | ||||
|                 class="border-base-content bg-base-300 rounded-btn border border-opacity-5" | ||||
|                 alt={item.title} | ||||
|               /> | ||||
| @@ -25,13 +25,13 @@ const { posts } = Astro.props; | ||||
|           )} | ||||
|           <div class="card-body"> | ||||
|             <h2 class="text-xl">{item.title}</h2> | ||||
|             <div> | ||||
|             <div class="mx-[-2px] mt-[-4px]"> | ||||
|               <span class="badge badge-accent">{POST_TYPES[item.type]}</span> | ||||
|               {item.categories?.map((category: string) => ( | ||||
|                 <span class="badge badge-primary">{category}</span> | ||||
|               {item.categories?.map((category: any) => ( | ||||
|                 <span class="badge badge-primary">{category.name}</span> | ||||
|               ))} | ||||
|               {item.tags?.map((tag: string) => ( | ||||
|                 <span class="badge badge-secondary">{tag}</span> | ||||
|               {item.tags?.map((tag: any) => ( | ||||
|                 <span class="badge badge-secondary">{tag.name}</span> | ||||
|               ))} | ||||
|             </div> | ||||
|             <div class="text-xs opacity-60 line-clamp-3"> | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| import { useState, Fragment } from "react"; | ||||
| 
 | ||||
| export default function Video({ | ||||
| export default function Media({ | ||||
|   sources, | ||||
| }: { | ||||
|   sources: { caption: string; url: string }[]; | ||||
|   sources: { caption: string; url: string; type: string }[]; | ||||
| }) { | ||||
|   const [focus, setFocus] = useState<boolean[]>( | ||||
|     sources.map((_, idx) => idx === 0) | ||||
| @@ -30,9 +30,16 @@ export default function Video({ | ||||
|             role="tabpanel" | ||||
|             className="tab-content bg-base-100 border-base-300 rounded-box" | ||||
|           > | ||||
|             <video className="mb-0 block w-full h-[360px]" controls> | ||||
|             {item.type === "video" && ( | ||||
|               <video className="mb-0 block w-full h-[360px]" controls> | ||||
|                 <source src={item.url} /> | ||||
|             </video> | ||||
|               </video> | ||||
|             )} | ||||
|             {item.type === "audio" && ( | ||||
|               <audio className="mb-0 block w-full h-[20px]" controls> | ||||
|                 <source src={item.url} /> | ||||
|               </audio> | ||||
|             )} | ||||
|           </div> | ||||
|         </Fragment> | ||||
|       ))} | ||||
| @@ -1,33 +1,48 @@ | ||||
| --- | ||||
| import PageLayout from "../../layouts/PageLayout.astro"; | ||||
|  | ||||
| import { client } from "../../../tina/__generated__/client"; | ||||
| import PostList from "../../components/PostList.astro"; | ||||
|  | ||||
| import { graphQuery } from "../../scripts/requests"; | ||||
|  | ||||
| export const prerender = false; | ||||
|  | ||||
| const { slug } = Astro.params; | ||||
|  | ||||
| const postsResponse = await client.queries.postConnection({ | ||||
|   filter: { categories: { in: [slug ?? "index"] } }, | ||||
| }); | ||||
| const posts = postsResponse.data.postConnection.edges | ||||
|   ?.sort((a, b) => | ||||
|     new Date(a?.node?.date ?? 0).getTime() <= | ||||
|     new Date(b?.node?.date ?? 0).getTime() | ||||
|       ? -1 | ||||
|       : 0 | ||||
| const { posts } = ( | ||||
|   await graphQuery( | ||||
|     `query Query($where: PostWhereInput!) { | ||||
|   posts(where: $where) { | ||||
|     slug | ||||
|     type | ||||
|     title | ||||
|     description | ||||
|     cover { | ||||
|       image { | ||||
|         url | ||||
|       } | ||||
|     } | ||||
|     content { | ||||
|       document | ||||
|     } | ||||
|     categories { | ||||
|       name | ||||
|     } | ||||
|     tags { | ||||
|       name | ||||
|     } | ||||
|     createdAt | ||||
|   } | ||||
| }`, | ||||
|     { where: { categories: { some: { slug: { equals: slug } } } } } | ||||
|   ) | ||||
|   .map((event) => { | ||||
|     return { ...event?.node, slug: event?.node?._sys.filename }; | ||||
|   }); | ||||
| ).data; | ||||
| --- | ||||
|  | ||||
| <PageLayout> | ||||
|   <div class="max-w-[720px] mx-auto"> | ||||
|     <div class="pt-16 pb-6 px-6"> | ||||
|       <h1 class="text-4xl font-bold">分类检索</h1> | ||||
|       <p class="pt-3">以下是包含「{slug}」分类的记录……</p> | ||||
|       <p class="pt-3">以下是包含该分类的记录……</p> | ||||
|     </div> | ||||
|  | ||||
|     <PostList posts={posts as any[]} /> | ||||
|   | ||||
| @@ -1,29 +1,40 @@ | ||||
| --- | ||||
| import PageLayout from "../../layouts/PageLayout.astro"; | ||||
|  | ||||
| import { client } from "../../../tina/__generated__/client"; | ||||
| import { TinaMarkdown } from "tinacms/dist/rich-text"; | ||||
| import { graphQuery } from "../../scripts/requests"; | ||||
| import { DocumentRenderer } from "@keystone-6/document-renderer"; | ||||
|  | ||||
| export const prerender = false; | ||||
|  | ||||
| const eventsResponse = await client.queries.eventConnection(); | ||||
| const events = eventsResponse.data.eventConnection.edges | ||||
|   ?.sort((a, b) => | ||||
|     new Date(a?.node?.date ?? 0).getTime() <= | ||||
|     new Date(b?.node?.date ?? 0).getTime() | ||||
|       ? -1 | ||||
|       : 0 | ||||
| const { events } = ( | ||||
|   await graphQuery( | ||||
|     `query Query($where: EventWhereInput!) { | ||||
|   events(where: $where) { | ||||
|     slug | ||||
|     title | ||||
|     description | ||||
|     content { | ||||
|       document | ||||
|     } | ||||
|     createdAt | ||||
|   } | ||||
| }`, | ||||
|     { | ||||
|       where: { | ||||
|         isHistory: { | ||||
|           equals: true, | ||||
|         }, | ||||
|       }, | ||||
|     } | ||||
|   ) | ||||
|   .map((event) => { | ||||
|     return { ...event?.node, slug: event?.node?._sys.filename }; | ||||
|   }); | ||||
| ).data; | ||||
| --- | ||||
|  | ||||
| <PageLayout> | ||||
|   <div class="max-w-[720px] mx-auto"> | ||||
|     <div class="card w-full shadow-xl"> | ||||
|       <div class="card-body"> | ||||
|         <h2 class="card-title">情报</h2> | ||||
|         <h2 class="card-title">活动</h2> | ||||
|         <p>读岁月史书,涨人生阅历</p> | ||||
|         <div class="divider"></div> | ||||
|  | ||||
| @@ -32,8 +43,8 @@ const events = eventsResponse.data.eventConnection.edges | ||||
|         > | ||||
|           { | ||||
|             events?.map((item: any, idx: number) => { | ||||
|               let align = idx % 2 === 0 ? "start" : "end"; | ||||
|               let textAlign = idx % 2 === 0 ? "left" : "right"; | ||||
|               let align = idx % 2 === 0 ? "timeline-start" : "timeline-end"; | ||||
|               let textAlign = idx % 2 === 0 ? "md:text-right" : "md:text-left"; | ||||
|  | ||||
|               return ( | ||||
|                 <li> | ||||
| @@ -52,12 +63,12 @@ const events = eventsResponse.data.eventConnection.edges | ||||
|                       /> | ||||
|                     </svg> | ||||
|                   </div> | ||||
|                   <div class={`timeline-${align} md:text-${textAlign} mb-10`}> | ||||
|                   <div class={`${align} ${textAlign} mb-10`}> | ||||
|                     <time class="font-mono italic"> | ||||
|                       {new Date(item.date).toLocaleDateString()} | ||||
|                       {new Date(item.createdAt).toLocaleDateString()} | ||||
|                     </time> | ||||
|                     <div class="text-lg font-black">{item.title}</div> | ||||
|                     <TinaMarkdown content={item._body} /> | ||||
|                     <DocumentRenderer document={item.content.document} /> | ||||
|                   </div> | ||||
|                   <hr /> | ||||
|                 </li> | ||||
| @@ -65,8 +76,10 @@ const events = eventsResponse.data.eventConnection.edges | ||||
|             }) | ||||
|           } | ||||
|         </ul> | ||||
|         <div class="text-center max-md:text-left italic">我们的故事还在继续……</div> | ||||
|         <div class="text-center max-md:text-left italic"> | ||||
|           我们的故事还在继续…… | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </PageLayout> | ||||
| </PageLayout> | ||||
| @@ -1,19 +1,27 @@ | ||||
| --- | ||||
| import RootLayout from "../layouts/RootLayout.astro"; | ||||
|  | ||||
| import { client } from "../../tina/__generated__/client"; | ||||
| import { graphQuery } from "../scripts/requests"; | ||||
|  | ||||
| const eventsResponse = await client.queries.eventConnection(); | ||||
| const events = eventsResponse.data.eventConnection.edges | ||||
|   ?.sort((a, b) => | ||||
|     new Date(a?.node?.date ?? 0).getTime() <= | ||||
|     new Date(b?.node?.date ?? 0).getTime() | ||||
|       ? -1 | ||||
|       : 0 | ||||
| const { events } = ( | ||||
|   await graphQuery( | ||||
|     `query Query($where: EventWhereInput!) { | ||||
|   events(where: $where) { | ||||
|     slug | ||||
|     title | ||||
|     description | ||||
|     createdAt | ||||
|   } | ||||
| }`, | ||||
|     { | ||||
|       where: { | ||||
|         isHistory: { | ||||
|           equals: true, | ||||
|         }, | ||||
|       }, | ||||
|     } | ||||
|   ) | ||||
|   .map((event) => { | ||||
|     return { ...event?.node, slug: event?.node?._sys.filename }; | ||||
|   }); | ||||
| ).data; | ||||
| --- | ||||
|  | ||||
| <RootLayout> | ||||
| @@ -140,7 +148,7 @@ const events = eventsResponse.data.eventConnection.edges | ||||
|                 <li> | ||||
|                   {idx > 0 && <hr />} | ||||
|                   <div class="timeline-start"> | ||||
|                     {new Date(item.date).toLocaleDateString()} | ||||
|                     {new Date(item.createdAt).toLocaleDateString()} | ||||
|                   </div> | ||||
|                   <div class="timeline-middle"> | ||||
|                     <svg | ||||
|   | ||||
| @@ -1,41 +1,78 @@ | ||||
| --- | ||||
| import PageLayout from "../../layouts/PageLayout.astro"; | ||||
| import Video from "../../components/posts/Video.tsx"; | ||||
| // @ts-ignore | ||||
| import Media from "../../components/posts/Media"; | ||||
|  | ||||
| import { POST_TYPES } from "../../scripts/consts"; | ||||
| import { client } from "../../../tina/__generated__/client"; | ||||
| import { TinaMarkdown } from "tinacms/dist/rich-text"; | ||||
| import { graphQuery } from "../../scripts/requests"; | ||||
| import { DocumentRenderer } from "@keystone-6/document-renderer"; | ||||
|  | ||||
| export const prerender = false; | ||||
|  | ||||
| const { slug } = Astro.params; | ||||
|  | ||||
| const components = { | ||||
|   Video, | ||||
| } | ||||
|  | ||||
| const { data } = await client.queries.post({ | ||||
|   relativePath: (slug ?? "index") + ".mdx", | ||||
| }); | ||||
| const { post } = ( | ||||
|   await graphQuery( | ||||
|     `query Query($where: PostWhereUniqueInput!) { | ||||
|   post(where: $where) { | ||||
|     slug | ||||
|     type | ||||
|     title | ||||
|     description | ||||
|     assets { | ||||
|       caption | ||||
|       url | ||||
|       type | ||||
|     } | ||||
|     cover { | ||||
|       image { | ||||
|         url | ||||
|       } | ||||
|     } | ||||
|     content { | ||||
|       document | ||||
|     } | ||||
|     categories { | ||||
|       slug | ||||
|       name | ||||
|     } | ||||
|     tags { | ||||
|       slug | ||||
|       name | ||||
|     } | ||||
|     createdAt | ||||
|   } | ||||
| }`, | ||||
|     { where: { slug } } | ||||
|   ) | ||||
| ).data; | ||||
| --- | ||||
|  | ||||
| <PageLayout> | ||||
|   <div class="wrapper"> | ||||
|     <div class="card w-full shadow-xl"> | ||||
|       { | ||||
|         data.post.heroImg && ( | ||||
|         post.cover != null && ( | ||||
|           <figure> | ||||
|             <img src={data.post.heroImg} alt={data.post.title} /> | ||||
|             <img src={post.cover.image.url} alt={post.title} /> | ||||
|           </figure> | ||||
|         ) | ||||
|       } | ||||
|       <div class="card-body"> | ||||
|         <h2 class="card-title">{data.post.title}</h2> | ||||
|         <p class="description">{data.post.description ?? "No description"}</p> | ||||
|         <h2 class="card-title">{post.title}</h2> | ||||
|         <p class="description">{post.description ?? "No description"}</p> | ||||
|         <div class="divider"></div> | ||||
|  | ||||
|         { | ||||
|           post.assets?.length > 0 && ( | ||||
|             <div class="mb-5"> | ||||
|               <Media sources={post.assets} /> | ||||
|             </div> | ||||
|           ) | ||||
|         } | ||||
|  | ||||
|         <div class="prose max-w-none"> | ||||
|           <TinaMarkdown content={data.post._body} components={components} /> | ||||
|           <DocumentRenderer document={post.content.document} /> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| @@ -46,19 +83,24 @@ const { data } = await client.queries.post({ | ||||
|           <div class="gap-2 text-sm metadata description"> | ||||
|             <div> | ||||
|               <div>作者</div> | ||||
|               <div>{data.post.author?.name ?? "佚名"}</div> | ||||
|               <div>{post.author?.name ?? "佚名"}</div> | ||||
|             </div> | ||||
|             <div> | ||||
|               <div>类型</div> | ||||
|               <div class="text-accent">{POST_TYPES[data.post.type as unknown as string]}</div> | ||||
|               <div class="text-accent"> | ||||
|                 {POST_TYPES[post.type as unknown as string]} | ||||
|               </div> | ||||
|             </div> | ||||
|             <div> | ||||
|               <div>分类</div> | ||||
|               <div class="flex gap-1"> | ||||
|                 { | ||||
|                   data.post.categories?.map((category) => ( | ||||
|                     <a href={`/categories/${category}`} class="link link-primary"> | ||||
|                       {category} | ||||
|                   post.categories?.map((category: any) => ( | ||||
|                     <a | ||||
|                       href={`/categories/${category.slug}`} | ||||
|                       class="link link-primary" | ||||
|                     > | ||||
|                       {category.name} | ||||
|                     </a> | ||||
|                   )) | ||||
|                 } | ||||
| @@ -68,9 +110,9 @@ const { data } = await client.queries.post({ | ||||
|               <div>标签</div> | ||||
|               <div class="flex gap-1"> | ||||
|                 { | ||||
|                   data.post.tags?.map((tag) => ( | ||||
|                     <a href={`/tags/${tag}`} class="link link-secondary"> | ||||
|                       {tag} | ||||
|                   post.tags?.map((tag: any) => ( | ||||
|                     <a href={`/tags/${tag.slug}`} class="link link-secondary"> | ||||
|                       {tag.name} | ||||
|                     </a> | ||||
|                   )) | ||||
|                 } | ||||
| @@ -78,7 +120,7 @@ const { data } = await client.queries.post({ | ||||
|             </div> | ||||
|             <div> | ||||
|               <div>发布于</div> | ||||
|               <div>{new Date(data.post.date ?? 0).toLocaleString()}</div> | ||||
|               <div>{new Date(post.createdAt).toLocaleString()}</div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|   | ||||
| @@ -1,30 +1,48 @@ | ||||
| --- | ||||
| import PageLayout from "../../layouts/PageLayout.astro"; | ||||
|  | ||||
| import { client } from "../../../tina/__generated__/client"; | ||||
| import PostList from "../../components/PostList.astro"; | ||||
|  | ||||
| import { graphQuery } from "../../scripts/requests"; | ||||
|  | ||||
| export const prerender = false; | ||||
|  | ||||
| const postsResponse = await client.queries.postConnection(); | ||||
| const posts = postsResponse.data.postConnection.edges | ||||
|   ?.sort((a, b) => | ||||
|     new Date(a?.node?.date ?? 0).getTime() <= | ||||
|     new Date(b?.node?.date ?? 0).getTime() | ||||
|       ? -1 | ||||
|       : 0 | ||||
| const { posts } = ( | ||||
|   await graphQuery( | ||||
|     `query Query($where: PostWhereInput!) { | ||||
|   posts(where: $where) { | ||||
|     slug | ||||
|     type | ||||
|     title | ||||
|     description | ||||
|     cover { | ||||
|       image { | ||||
|         url | ||||
|       } | ||||
|     } | ||||
|     content { | ||||
|       document | ||||
|     } | ||||
|     categories { | ||||
|       name | ||||
|     } | ||||
|     tags { | ||||
|       name | ||||
|     } | ||||
|     createdAt | ||||
|   } | ||||
| }`, | ||||
|     { where: {} } | ||||
|   ) | ||||
|   .map((event) => { | ||||
|     return { ...event?.node, slug: event?.node?._sys.filename }; | ||||
|   }); | ||||
| ).data; | ||||
| --- | ||||
|  | ||||
| <PageLayout> | ||||
|   <div class="max-w-[720px] mx-auto"> | ||||
|     <div class="pt-16 pb-6 px-6"> | ||||
|       <h1 class="text-4xl font-bold">记录</h1> | ||||
|       <p class="pt-3">记录生活,记录理想,记录记录……</p> | ||||
|       <p class="pt-2">记录生活,记录理想,记录记录……</p> | ||||
|     </div> | ||||
|  | ||||
|     <PostList posts={posts as any[]} /> | ||||
|   </div> | ||||
| </PageLayout> | ||||
|   | ||||
| @@ -1,33 +1,48 @@ | ||||
| --- | ||||
| import PageLayout from "../../layouts/PageLayout.astro"; | ||||
|  | ||||
| import { client } from "../../../tina/__generated__/client"; | ||||
| import PostList from "../../components/PostList.astro"; | ||||
|  | ||||
| import { graphQuery } from "../../scripts/requests"; | ||||
|  | ||||
| export const prerender = false; | ||||
|  | ||||
| const { slug } = Astro.params; | ||||
|  | ||||
| const postsResponse = await client.queries.postConnection({ | ||||
|   filter: { tags: { in: [slug ?? "index"] } }, | ||||
| }); | ||||
| const posts = postsResponse.data.postConnection.edges | ||||
|   ?.sort((a, b) => | ||||
|     new Date(a?.node?.date ?? 0).getTime() <= | ||||
|     new Date(b?.node?.date ?? 0).getTime() | ||||
|       ? -1 | ||||
|       : 0 | ||||
| const { posts } = ( | ||||
|   await graphQuery( | ||||
|     `query Query($where: PostWhereInput!) { | ||||
|   posts(where: $where) { | ||||
|     slug | ||||
|     type | ||||
|     title | ||||
|     description | ||||
|     cover { | ||||
|       image { | ||||
|         url | ||||
|       } | ||||
|     } | ||||
|     content { | ||||
|       document | ||||
|     } | ||||
|     categories { | ||||
|       name | ||||
|     } | ||||
|     tags { | ||||
|       name | ||||
|     } | ||||
|     createdAt | ||||
|   } | ||||
| }`, | ||||
|     { where: { tags: { some: { slug: { equals: slug } } } } } | ||||
|   ) | ||||
|   .map((event) => { | ||||
|     return { ...event?.node, slug: event?.node?._sys.filename }; | ||||
|   }); | ||||
| ).data; | ||||
| --- | ||||
|  | ||||
| <PageLayout> | ||||
|   <div class="max-w-[720px] mx-auto"> | ||||
|     <div class="pt-16 pb-6 px-6"> | ||||
|       <h1 class="text-4xl font-bold">标签检索</h1> | ||||
|       <p class="pt-3">以下是包含「{slug}」标签的记录……</p> | ||||
|       <p class="pt-3">以下是包含该标签的记录……</p> | ||||
|     </div> | ||||
|  | ||||
|     <PostList posts={posts as any[]} /> | ||||
|   | ||||
							
								
								
									
										15
									
								
								src/scripts/requests.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/scripts/requests.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| export async function graphQuery(query: string, variables: any) { | ||||
|   const response = await fetch( | ||||
|     `${import.meta.env.PUBLIC_CMS}/api/graphql`, | ||||
|     { | ||||
|       method: "POST", | ||||
|       headers: { "Content-Type": "application/json" }, | ||||
|       body: JSON.stringify({ | ||||
|         query, | ||||
|         variables, | ||||
|       }), | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|   return await response.json(); | ||||
| } | ||||
							
								
								
									
										1
									
								
								tina/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								tina/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1 +0,0 @@ | ||||
| __generated__ | ||||
| @@ -1,30 +0,0 @@ | ||||
| import type { Collection } from "tinacms"; | ||||
|  | ||||
| const Author: Collection = { | ||||
|   label: "Authors", | ||||
|   name: "author", | ||||
|   path: "content/authors", | ||||
|   format: "mdx", | ||||
|   fields: [ | ||||
|     { | ||||
|       type: "string", | ||||
|       label: "Name", | ||||
|       name: "name", | ||||
|       isTitle: true, | ||||
|       required: true, | ||||
|     }, | ||||
|     { | ||||
|       type: "image", | ||||
|       label: "Avatar", | ||||
|       name: "avatar", | ||||
|     }, | ||||
|     { | ||||
|       type: "rich-text", | ||||
|       label: "Introduction", | ||||
|       name: "_body", | ||||
|       templates: [], | ||||
|       isBody: true, | ||||
|     }, | ||||
|   ], | ||||
| }; | ||||
| export default Author; | ||||
| @@ -1,56 +0,0 @@ | ||||
| import type { Collection } from "tinacms"; | ||||
|  | ||||
| const Event: Collection = { | ||||
|   label: "Events", | ||||
|   name: "event", | ||||
|   path: "content/events", | ||||
|   format: "mdx", | ||||
|   ui: { | ||||
|     router: ({ document }) => { | ||||
|       return `/events/${document._sys.filename}`; | ||||
|     }, | ||||
|   }, | ||||
|   fields: [ | ||||
|     { | ||||
|       type: "string", | ||||
|       label: "Title", | ||||
|       name: "title", | ||||
|       isTitle: true, | ||||
|       required: true, | ||||
|     }, | ||||
|     { | ||||
|       type: "image", | ||||
|       name: "heroImg", | ||||
|       label: "Hero Image", | ||||
|     }, | ||||
|     { | ||||
|       type: "reference", | ||||
|       label: "Author", | ||||
|       name: "author", | ||||
|       collections: ["author"], | ||||
|     }, | ||||
|     { | ||||
|       type: "string", | ||||
|       label: "Description", | ||||
|       name: "description", | ||||
|     }, | ||||
|     { | ||||
|       type: "datetime", | ||||
|       label: "Published Date", | ||||
|       name: "date", | ||||
|       ui: { | ||||
|         dateFormat: "MMMM DD YYYY", | ||||
|         timeFormat: "hh:mm A", | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       type: "rich-text", | ||||
|       label: "Body", | ||||
|       name: "_body", | ||||
|       templates: [], | ||||
|       isBody: true, | ||||
|     }, | ||||
|   ], | ||||
| }; | ||||
|  | ||||
| export default Event; | ||||
| @@ -1,36 +0,0 @@ | ||||
| import type { Collection } from "tinacms"; | ||||
|  | ||||
| const Page: Collection = { | ||||
|   label: "Pages", | ||||
|   name: "page", | ||||
|   path: "content/pages", | ||||
|   format: "mdx", | ||||
|   ui: { | ||||
|     router: ({ document }) => { | ||||
|       if (document._sys.filename === "about") { | ||||
|         return `/about`; | ||||
|       } | ||||
|       return undefined; | ||||
|     }, | ||||
|   }, | ||||
|   fields: [ | ||||
|     { | ||||
|       type: "string", | ||||
|       label: "Title", | ||||
|       name: "title", | ||||
|       description: | ||||
|         "The title of the page. This is used to display the title in the CMS", | ||||
|       isTitle: true, | ||||
|       required: true, | ||||
|     }, | ||||
|     { | ||||
|       type: "rich-text", | ||||
|       label: "Body", | ||||
|       name: "_body", | ||||
|       templates: [], | ||||
|       isBody: true, | ||||
|     }, | ||||
|   ], | ||||
| }; | ||||
|  | ||||
| export default Page; | ||||
| @@ -1,102 +0,0 @@ | ||||
| import type { Collection } from "tinacms"; | ||||
|  | ||||
| const Post: Collection = { | ||||
|   label: "Posts", | ||||
|   name: "post", | ||||
|   path: "content/posts", | ||||
|   format: "mdx", | ||||
|   ui: { | ||||
|     router: ({ document }) => { | ||||
|       return `/posts/${document._sys.filename}`; | ||||
|     }, | ||||
|   }, | ||||
|   fields: [ | ||||
|     { | ||||
|       type: "string", | ||||
|       label: "Title", | ||||
|       name: "title", | ||||
|       isTitle: true, | ||||
|       required: true, | ||||
|     }, | ||||
|     { | ||||
|       type: "image", | ||||
|       name: "heroImg", | ||||
|       label: "Hero Image", | ||||
|     }, | ||||
|     { | ||||
|       type: "reference", | ||||
|       label: "Author", | ||||
|       name: "author", | ||||
|       collections: ["author"], | ||||
|     }, | ||||
|     { | ||||
|       type: "string", | ||||
|       label: "Description", | ||||
|       name: "description", | ||||
|     }, | ||||
|     { | ||||
|       type: "datetime", | ||||
|       label: "Published Date", | ||||
|       name: "date", | ||||
|       ui: { | ||||
|         dateFormat: "MMMM DD YYYY", | ||||
|         timeFormat: "hh:mm A", | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       label: "Categories", | ||||
|       name: "categories", | ||||
|       type: "string", | ||||
|       list: true, | ||||
|     }, | ||||
|     { | ||||
|       label: "Tags", | ||||
|       name: "tags", | ||||
|       type: "string", | ||||
|       list: true, | ||||
|     }, | ||||
|     { | ||||
|       label: "Type", | ||||
|       name: "type", | ||||
|       type: "string", | ||||
|       // @ts-ignore | ||||
|       component: "select", | ||||
|       options: ["article", "podcast", "announcement"], | ||||
|       list: true, | ||||
|     }, | ||||
|     { | ||||
|       type: "rich-text", | ||||
|       label: "Body", | ||||
|       name: "_body", | ||||
|       templates: [ | ||||
|         { | ||||
|           name: "Video", | ||||
|           label: "Video", | ||||
|           fields: [ | ||||
|             { | ||||
|               name: "sources", | ||||
|               label: "Sources", | ||||
|               type: "object", | ||||
|               fields: [ | ||||
|                 { | ||||
|                   name: "caption", | ||||
|                   label: "Caption", | ||||
|                   type: "string", | ||||
|                 }, | ||||
|                 { | ||||
|                   name: "url", | ||||
|                   label: "URL", | ||||
|                   type: "string", | ||||
|                 }, | ||||
|               ], | ||||
|               list: true, | ||||
|             }, | ||||
|           ], | ||||
|         }, | ||||
|       ], | ||||
|       isBody: true, | ||||
|     }, | ||||
|   ], | ||||
| }; | ||||
|  | ||||
| export default Post; | ||||
| @@ -1,41 +0,0 @@ | ||||
| import { defineConfig } from "tinacms"; | ||||
| import Author from "./collection/author"; | ||||
| import Event from "./collection/event"; | ||||
| import Post from "./collection/post"; | ||||
| import Page from "./collection/page"; | ||||
|  | ||||
| // Your hosting provider likely exposes this as an environment variable | ||||
| const branch = | ||||
|   process.env.GITHUB_BRANCH || | ||||
|   process.env.VERCEL_GIT_COMMIT_REF || | ||||
|   process.env.HEAD || | ||||
|   "main"; | ||||
|  | ||||
| export default defineConfig({ | ||||
|   branch, | ||||
|  | ||||
|   // Get this from tina.io | ||||
|   clientId: process.env.NEXT_PUBLIC_TINA_CLIENT_ID, | ||||
|   // Get this from tina.io | ||||
|   token: process.env.TINA_TOKEN, | ||||
|  | ||||
|   build: { | ||||
|     outputFolder: "admin", | ||||
|     publicFolder: "public", | ||||
|   }, | ||||
|   media: { | ||||
|     tina: { | ||||
|       mediaRoot: "media", | ||||
|       publicFolder: "public", | ||||
|     }, | ||||
|   }, | ||||
|   // See docs on content modeling for more info on how to setup new content models: https://tina.io/docs/schema/ | ||||
|   schema: { | ||||
|     collections: [ | ||||
|       Author, | ||||
|       Event, | ||||
|       Page, | ||||
|       Post, | ||||
|     ], | ||||
|   }, | ||||
| }); | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -2,9 +2,6 @@ | ||||
|   "extends": "astro/tsconfigs/strict", | ||||
|   "compilerOptions": { | ||||
|     "jsx": "react-jsx", | ||||
|     "jsxImportSource": "react" | ||||
|     "jsxImportSource": "react", | ||||
|   }, | ||||
|   "paths": { | ||||
|     "@/*": ["src/*"], | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user