♻️ Use keystonejs
This commit is contained in:
parent
f0063afffa
commit
16aa7fd215
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
|
.DS_Store
|
||||||
|
|
||||||
# Development content
|
# Development content
|
||||||
content
|
|
||||||
public/media
|
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",
|
"type": "module",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tinacms dev -c \"astro dev\"",
|
"dev": "astro dev",
|
||||||
"start": "astro dev",
|
"start": "astro dev",
|
||||||
"build": "astro check && astro build",
|
"build": "astro check && astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
@ -16,6 +16,7 @@
|
|||||||
"@astrojs/react": "^3.0.9",
|
"@astrojs/react": "^3.0.9",
|
||||||
"@astrojs/sitemap": "^3.0.5",
|
"@astrojs/sitemap": "^3.0.5",
|
||||||
"@astrojs/tailwind": "^5.1.0",
|
"@astrojs/tailwind": "^5.1.0",
|
||||||
|
"@keystone-6/document-renderer": "^1.1.2",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.2.48",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.18",
|
||||||
@ -24,12 +25,10 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"sass": "^1.70.0",
|
"sass": "^1.70.0",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"tinacms": "^1.5.28",
|
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.10",
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
"@tinacms/cli": "^1.5.39",
|
|
||||||
"@types/node": "^20.11.5",
|
"@types/node": "^20.11.5",
|
||||||
"daisyui": "^4.6.0",
|
"daisyui": "^4.6.0",
|
||||||
"prettier": "^3.2.4"
|
"prettier": "^3.2.4"
|
||||||
|
@ -7,7 +7,7 @@ interface MenuItem {
|
|||||||
|
|
||||||
const items: MenuItem[] = [
|
const items: MenuItem[] = [
|
||||||
{ href: "/posts", label: "记录" },
|
{ href: "/posts", label: "记录" },
|
||||||
{ href: "/events", label: "情报" },
|
{ href: "/events", label: "活动" },
|
||||||
{ href: "/projects", label: "企划" },
|
{ href: "/projects", label: "企划" },
|
||||||
];
|
];
|
||||||
---
|
---
|
||||||
|
@ -13,11 +13,11 @@ const { posts } = Astro.props;
|
|||||||
posts?.map((item) => (
|
posts?.map((item) => (
|
||||||
<a href={`/posts/${item.slug}`}>
|
<a href={`/posts/${item.slug}`}>
|
||||||
<div class="card sm:card-side hover:bg-base-200 transition-colors sm:max-w-none shadow-xl">
|
<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">
|
<figure class="mx-auto w-full object-cover p-6 max-sm:pb-0 sm:max-w-[12rem] sm:pe-0">
|
||||||
<img
|
<img
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
src={item.heroImg}
|
src={item.cover.image.url}
|
||||||
class="border-base-content bg-base-300 rounded-btn border border-opacity-5"
|
class="border-base-content bg-base-300 rounded-btn border border-opacity-5"
|
||||||
alt={item.title}
|
alt={item.title}
|
||||||
/>
|
/>
|
||||||
@ -25,13 +25,13 @@ const { posts } = Astro.props;
|
|||||||
)}
|
)}
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="text-xl">{item.title}</h2>
|
<h2 class="text-xl">{item.title}</h2>
|
||||||
<div>
|
<div class="mx-[-2px] mt-[-4px]">
|
||||||
<span class="badge badge-accent">{POST_TYPES[item.type]}</span>
|
<span class="badge badge-accent">{POST_TYPES[item.type]}</span>
|
||||||
{item.categories?.map((category: string) => (
|
{item.categories?.map((category: any) => (
|
||||||
<span class="badge badge-primary">{category}</span>
|
<span class="badge badge-primary">{category.name}</span>
|
||||||
))}
|
))}
|
||||||
{item.tags?.map((tag: string) => (
|
{item.tags?.map((tag: any) => (
|
||||||
<span class="badge badge-secondary">{tag}</span>
|
<span class="badge badge-secondary">{tag.name}</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs opacity-60 line-clamp-3">
|
<div class="text-xs opacity-60 line-clamp-3">
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { useState, Fragment } from "react";
|
import { useState, Fragment } from "react";
|
||||||
|
|
||||||
export default function Video({
|
export default function Media({
|
||||||
sources,
|
sources,
|
||||||
}: {
|
}: {
|
||||||
sources: { caption: string; url: string }[];
|
sources: { caption: string; url: string; type: string }[];
|
||||||
}) {
|
}) {
|
||||||
const [focus, setFocus] = useState<boolean[]>(
|
const [focus, setFocus] = useState<boolean[]>(
|
||||||
sources.map((_, idx) => idx === 0)
|
sources.map((_, idx) => idx === 0)
|
||||||
@ -30,9 +30,16 @@ export default function Video({
|
|||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
className="tab-content bg-base-100 border-base-300 rounded-box"
|
className="tab-content bg-base-100 border-base-300 rounded-box"
|
||||||
>
|
>
|
||||||
|
{item.type === "video" && (
|
||||||
<video className="mb-0 block w-full h-[360px]" controls>
|
<video className="mb-0 block w-full h-[360px]" controls>
|
||||||
<source src={item.url} />
|
<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>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
@ -1,33 +1,48 @@
|
|||||||
---
|
---
|
||||||
import PageLayout from "../../layouts/PageLayout.astro";
|
import PageLayout from "../../layouts/PageLayout.astro";
|
||||||
|
|
||||||
import { client } from "../../../tina/__generated__/client";
|
|
||||||
import PostList from "../../components/PostList.astro";
|
import PostList from "../../components/PostList.astro";
|
||||||
|
|
||||||
|
import { graphQuery } from "../../scripts/requests";
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
const { slug } = Astro.params;
|
const { slug } = Astro.params;
|
||||||
|
|
||||||
const postsResponse = await client.queries.postConnection({
|
const { posts } = (
|
||||||
filter: { categories: { in: [slug ?? "index"] } },
|
await graphQuery(
|
||||||
});
|
`query Query($where: PostWhereInput!) {
|
||||||
const posts = postsResponse.data.postConnection.edges
|
posts(where: $where) {
|
||||||
?.sort((a, b) =>
|
slug
|
||||||
new Date(a?.node?.date ?? 0).getTime() <=
|
type
|
||||||
new Date(b?.node?.date ?? 0).getTime()
|
title
|
||||||
? -1
|
description
|
||||||
: 0
|
cover {
|
||||||
|
image {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content {
|
||||||
|
document
|
||||||
|
}
|
||||||
|
categories {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
tags {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
{ where: { categories: { some: { slug: { equals: slug } } } } }
|
||||||
)
|
)
|
||||||
.map((event) => {
|
).data;
|
||||||
return { ...event?.node, slug: event?.node?._sys.filename };
|
|
||||||
});
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div class="max-w-[720px] mx-auto">
|
<div class="max-w-[720px] mx-auto">
|
||||||
<div class="pt-16 pb-6 px-6">
|
<div class="pt-16 pb-6 px-6">
|
||||||
<h1 class="text-4xl font-bold">分类检索</h1>
|
<h1 class="text-4xl font-bold">分类检索</h1>
|
||||||
<p class="pt-3">以下是包含「{slug}」分类的记录……</p>
|
<p class="pt-3">以下是包含该分类的记录……</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PostList posts={posts as any[]} />
|
<PostList posts={posts as any[]} />
|
||||||
|
@ -1,29 +1,40 @@
|
|||||||
---
|
---
|
||||||
import PageLayout from "../../layouts/PageLayout.astro";
|
import PageLayout from "../../layouts/PageLayout.astro";
|
||||||
|
|
||||||
import { client } from "../../../tina/__generated__/client";
|
import { graphQuery } from "../../scripts/requests";
|
||||||
import { TinaMarkdown } from "tinacms/dist/rich-text";
|
import { DocumentRenderer } from "@keystone-6/document-renderer";
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
const eventsResponse = await client.queries.eventConnection();
|
const { events } = (
|
||||||
const events = eventsResponse.data.eventConnection.edges
|
await graphQuery(
|
||||||
?.sort((a, b) =>
|
`query Query($where: EventWhereInput!) {
|
||||||
new Date(a?.node?.date ?? 0).getTime() <=
|
events(where: $where) {
|
||||||
new Date(b?.node?.date ?? 0).getTime()
|
slug
|
||||||
? -1
|
title
|
||||||
: 0
|
description
|
||||||
|
content {
|
||||||
|
document
|
||||||
|
}
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
where: {
|
||||||
|
isHistory: {
|
||||||
|
equals: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
.map((event) => {
|
).data;
|
||||||
return { ...event?.node, slug: event?.node?._sys.filename };
|
|
||||||
});
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div class="max-w-[720px] mx-auto">
|
<div class="max-w-[720px] mx-auto">
|
||||||
<div class="card w-full shadow-xl">
|
<div class="card w-full shadow-xl">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title">情报</h2>
|
<h2 class="card-title">活动</h2>
|
||||||
<p>读岁月史书,涨人生阅历</p>
|
<p>读岁月史书,涨人生阅历</p>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
@ -32,8 +43,8 @@ const events = eventsResponse.data.eventConnection.edges
|
|||||||
>
|
>
|
||||||
{
|
{
|
||||||
events?.map((item: any, idx: number) => {
|
events?.map((item: any, idx: number) => {
|
||||||
let align = idx % 2 === 0 ? "start" : "end";
|
let align = idx % 2 === 0 ? "timeline-start" : "timeline-end";
|
||||||
let textAlign = idx % 2 === 0 ? "left" : "right";
|
let textAlign = idx % 2 === 0 ? "md:text-right" : "md:text-left";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
@ -52,12 +63,12 @@ const events = eventsResponse.data.eventConnection.edges
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class={`timeline-${align} md:text-${textAlign} mb-10`}>
|
<div class={`${align} ${textAlign} mb-10`}>
|
||||||
<time class="font-mono italic">
|
<time class="font-mono italic">
|
||||||
{new Date(item.date).toLocaleDateString()}
|
{new Date(item.createdAt).toLocaleDateString()}
|
||||||
</time>
|
</time>
|
||||||
<div class="text-lg font-black">{item.title}</div>
|
<div class="text-lg font-black">{item.title}</div>
|
||||||
<TinaMarkdown content={item._body} />
|
<DocumentRenderer document={item.content.document} />
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
</li>
|
</li>
|
||||||
@ -65,7 +76,9 @@ const events = eventsResponse.data.eventConnection.edges
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
<div class="text-center max-md:text-left italic">我们的故事还在继续……</div>
|
<div class="text-center max-md:text-left italic">
|
||||||
|
我们的故事还在继续……
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,19 +1,27 @@
|
|||||||
---
|
---
|
||||||
import RootLayout from "../layouts/RootLayout.astro";
|
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 } = (
|
||||||
const events = eventsResponse.data.eventConnection.edges
|
await graphQuery(
|
||||||
?.sort((a, b) =>
|
`query Query($where: EventWhereInput!) {
|
||||||
new Date(a?.node?.date ?? 0).getTime() <=
|
events(where: $where) {
|
||||||
new Date(b?.node?.date ?? 0).getTime()
|
slug
|
||||||
? -1
|
title
|
||||||
: 0
|
description
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
where: {
|
||||||
|
isHistory: {
|
||||||
|
equals: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
.map((event) => {
|
).data;
|
||||||
return { ...event?.node, slug: event?.node?._sys.filename };
|
|
||||||
});
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<RootLayout>
|
<RootLayout>
|
||||||
@ -140,7 +148,7 @@ const events = eventsResponse.data.eventConnection.edges
|
|||||||
<li>
|
<li>
|
||||||
{idx > 0 && <hr />}
|
{idx > 0 && <hr />}
|
||||||
<div class="timeline-start">
|
<div class="timeline-start">
|
||||||
{new Date(item.date).toLocaleDateString()}
|
{new Date(item.createdAt).toLocaleDateString()}
|
||||||
</div>
|
</div>
|
||||||
<div class="timeline-middle">
|
<div class="timeline-middle">
|
||||||
<svg
|
<svg
|
||||||
|
@ -1,41 +1,78 @@
|
|||||||
---
|
---
|
||||||
import PageLayout from "../../layouts/PageLayout.astro";
|
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 { POST_TYPES } from "../../scripts/consts";
|
||||||
import { client } from "../../../tina/__generated__/client";
|
import { graphQuery } from "../../scripts/requests";
|
||||||
import { TinaMarkdown } from "tinacms/dist/rich-text";
|
import { DocumentRenderer } from "@keystone-6/document-renderer";
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
const { slug } = Astro.params;
|
const { slug } = Astro.params;
|
||||||
|
|
||||||
const components = {
|
const { post } = (
|
||||||
Video,
|
await graphQuery(
|
||||||
}
|
`query Query($where: PostWhereUniqueInput!) {
|
||||||
|
post(where: $where) {
|
||||||
const { data } = await client.queries.post({
|
slug
|
||||||
relativePath: (slug ?? "index") + ".mdx",
|
type
|
||||||
});
|
title
|
||||||
|
description
|
||||||
|
assets {
|
||||||
|
caption
|
||||||
|
url
|
||||||
|
type
|
||||||
|
}
|
||||||
|
cover {
|
||||||
|
image {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content {
|
||||||
|
document
|
||||||
|
}
|
||||||
|
categories {
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
}
|
||||||
|
tags {
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
}
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
{ where: { slug } }
|
||||||
|
)
|
||||||
|
).data;
|
||||||
---
|
---
|
||||||
|
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div class="card w-full shadow-xl">
|
<div class="card w-full shadow-xl">
|
||||||
{
|
{
|
||||||
data.post.heroImg && (
|
post.cover != null && (
|
||||||
<figure>
|
<figure>
|
||||||
<img src={data.post.heroImg} alt={data.post.title} />
|
<img src={post.cover.image.url} alt={post.title} />
|
||||||
</figure>
|
</figure>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title">{data.post.title}</h2>
|
<h2 class="card-title">{post.title}</h2>
|
||||||
<p class="description">{data.post.description ?? "No description"}</p>
|
<p class="description">{post.description ?? "No description"}</p>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
{
|
||||||
|
post.assets?.length > 0 && (
|
||||||
|
<div class="mb-5">
|
||||||
|
<Media sources={post.assets} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
<div class="prose max-w-none">
|
<div class="prose max-w-none">
|
||||||
<TinaMarkdown content={data.post._body} components={components} />
|
<DocumentRenderer document={post.content.document} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -46,19 +83,24 @@ const { data } = await client.queries.post({
|
|||||||
<div class="gap-2 text-sm metadata description">
|
<div class="gap-2 text-sm metadata description">
|
||||||
<div>
|
<div>
|
||||||
<div>作者</div>
|
<div>作者</div>
|
||||||
<div>{data.post.author?.name ?? "佚名"}</div>
|
<div>{post.author?.name ?? "佚名"}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<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>分类</div>
|
<div>分类</div>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
{
|
{
|
||||||
data.post.categories?.map((category) => (
|
post.categories?.map((category: any) => (
|
||||||
<a href={`/categories/${category}`} class="link link-primary">
|
<a
|
||||||
{category}
|
href={`/categories/${category.slug}`}
|
||||||
|
class="link link-primary"
|
||||||
|
>
|
||||||
|
{category.name}
|
||||||
</a>
|
</a>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@ -68,9 +110,9 @@ const { data } = await client.queries.post({
|
|||||||
<div>标签</div>
|
<div>标签</div>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
{
|
{
|
||||||
data.post.tags?.map((tag) => (
|
post.tags?.map((tag: any) => (
|
||||||
<a href={`/tags/${tag}`} class="link link-secondary">
|
<a href={`/tags/${tag.slug}`} class="link link-secondary">
|
||||||
{tag}
|
{tag.name}
|
||||||
</a>
|
</a>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@ -78,7 +120,7 @@ const { data } = await client.queries.post({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div>发布于</div>
|
<div>发布于</div>
|
||||||
<div>{new Date(data.post.date ?? 0).toLocaleString()}</div>
|
<div>{new Date(post.createdAt).toLocaleString()}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,30 +1,48 @@
|
|||||||
---
|
---
|
||||||
import PageLayout from "../../layouts/PageLayout.astro";
|
import PageLayout from "../../layouts/PageLayout.astro";
|
||||||
|
|
||||||
import { client } from "../../../tina/__generated__/client";
|
|
||||||
import PostList from "../../components/PostList.astro";
|
import PostList from "../../components/PostList.astro";
|
||||||
|
|
||||||
|
import { graphQuery } from "../../scripts/requests";
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
const postsResponse = await client.queries.postConnection();
|
const { posts } = (
|
||||||
const posts = postsResponse.data.postConnection.edges
|
await graphQuery(
|
||||||
?.sort((a, b) =>
|
`query Query($where: PostWhereInput!) {
|
||||||
new Date(a?.node?.date ?? 0).getTime() <=
|
posts(where: $where) {
|
||||||
new Date(b?.node?.date ?? 0).getTime()
|
slug
|
||||||
? -1
|
type
|
||||||
: 0
|
title
|
||||||
|
description
|
||||||
|
cover {
|
||||||
|
image {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content {
|
||||||
|
document
|
||||||
|
}
|
||||||
|
categories {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
tags {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
{ where: {} }
|
||||||
)
|
)
|
||||||
.map((event) => {
|
).data;
|
||||||
return { ...event?.node, slug: event?.node?._sys.filename };
|
|
||||||
});
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div class="max-w-[720px] mx-auto">
|
<div class="max-w-[720px] mx-auto">
|
||||||
<div class="pt-16 pb-6 px-6">
|
<div class="pt-16 pb-6 px-6">
|
||||||
<h1 class="text-4xl font-bold">记录</h1>
|
<h1 class="text-4xl font-bold">记录</h1>
|
||||||
<p class="pt-3">记录生活,记录理想,记录记录……</p>
|
<p class="pt-2">记录生活,记录理想,记录记录……</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PostList posts={posts as any[]} />
|
<PostList posts={posts as any[]} />
|
||||||
|
</div>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
@ -1,33 +1,48 @@
|
|||||||
---
|
---
|
||||||
import PageLayout from "../../layouts/PageLayout.astro";
|
import PageLayout from "../../layouts/PageLayout.astro";
|
||||||
|
|
||||||
import { client } from "../../../tina/__generated__/client";
|
|
||||||
import PostList from "../../components/PostList.astro";
|
import PostList from "../../components/PostList.astro";
|
||||||
|
|
||||||
|
import { graphQuery } from "../../scripts/requests";
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
const { slug } = Astro.params;
|
const { slug } = Astro.params;
|
||||||
|
|
||||||
const postsResponse = await client.queries.postConnection({
|
const { posts } = (
|
||||||
filter: { tags: { in: [slug ?? "index"] } },
|
await graphQuery(
|
||||||
});
|
`query Query($where: PostWhereInput!) {
|
||||||
const posts = postsResponse.data.postConnection.edges
|
posts(where: $where) {
|
||||||
?.sort((a, b) =>
|
slug
|
||||||
new Date(a?.node?.date ?? 0).getTime() <=
|
type
|
||||||
new Date(b?.node?.date ?? 0).getTime()
|
title
|
||||||
? -1
|
description
|
||||||
: 0
|
cover {
|
||||||
|
image {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content {
|
||||||
|
document
|
||||||
|
}
|
||||||
|
categories {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
tags {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
{ where: { tags: { some: { slug: { equals: slug } } } } }
|
||||||
)
|
)
|
||||||
.map((event) => {
|
).data;
|
||||||
return { ...event?.node, slug: event?.node?._sys.filename };
|
|
||||||
});
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div class="max-w-[720px] mx-auto">
|
<div class="max-w-[720px] mx-auto">
|
||||||
<div class="pt-16 pb-6 px-6">
|
<div class="pt-16 pb-6 px-6">
|
||||||
<h1 class="text-4xl font-bold">标签检索</h1>
|
<h1 class="text-4xl font-bold">标签检索</h1>
|
||||||
<p class="pt-3">以下是包含「{slug}」标签的记录……</p>
|
<p class="pt-3">以下是包含该标签的记录……</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PostList posts={posts as any[]} />
|
<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",
|
"extends": "astro/tsconfigs/strict",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"jsxImportSource": "react"
|
"jsxImportSource": "react",
|
||||||
},
|
},
|
||||||
"paths": {
|
|
||||||
"@/*": ["src/*"],
|
|
||||||
}
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user